diff --git a/README.md b/README.md
index 5697f95d..8f48d6d3 100644
--- a/README.md
+++ b/README.md
@@ -2,6 +2,6 @@
 > Misuzu can and will steal your lunch money.
 
 ## Requirements
- - PHP 8.2 (64-bit)
+ - PHP 8.4
  - MariaDB 10.6
  - [Composer](https://getcomposer.org/)
diff --git a/composer.json b/composer.json
index a7e535e8..2cd4752a 100644
--- a/composer.json
+++ b/composer.json
@@ -1,5 +1,6 @@
 {
     "require": {
+        "php": ">=8.4",
         "flashwave/index": "^0.2410",
         "flashii/rpcii": "^2.0",
         "erusev/parsedown": "~1.6",
diff --git a/composer.lock b/composer.lock
index 22c9074a..2dcfd8c2 100644
--- a/composer.lock
+++ b/composer.lock
@@ -4,7 +4,7 @@
         "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
         "This file is @generated automatically"
     ],
-    "content-hash": "33116c436dc775be3d0e0b1d33114224",
+    "content-hash": "1bf2d030b7813e94e87ca04c39b83eff",
     "packages": [
         {
             "name": "carbonphp/carbon-doctrine-types",
@@ -77,29 +77,29 @@
         },
         {
             "name": "chillerlan/php-qrcode",
-            "version": "4.4.1",
+            "version": "4.4.2",
             "source": {
                 "type": "git",
                 "url": "https://github.com/chillerlan/php-qrcode.git",
-                "reference": "f5e243f3b61a60934780579430a951460f40888d"
+                "reference": "345ed8e4ffb56e6b3fcd9f42e3970b9026fa6ce4"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/chillerlan/php-qrcode/zipball/f5e243f3b61a60934780579430a951460f40888d",
-                "reference": "f5e243f3b61a60934780579430a951460f40888d",
+                "url": "https://api.github.com/repos/chillerlan/php-qrcode/zipball/345ed8e4ffb56e6b3fcd9f42e3970b9026fa6ce4",
+                "reference": "345ed8e4ffb56e6b3fcd9f42e3970b9026fa6ce4",
                 "shasum": ""
             },
             "require": {
-                "chillerlan/php-settings-container": "^2.1.4 || ^3.1",
+                "chillerlan/php-settings-container": "^2.1.6 || ^3.2.1",
                 "ext-mbstring": "*",
                 "php": "^7.4 || ^8.0"
             },
             "require-dev": {
-                "phan/phan": "^5.4",
+                "phan/phan": "^5.4.5",
                 "phpmd/phpmd": "^2.15",
                 "phpunit/phpunit": "^9.6",
                 "setasign/fpdf": "^1.8.2",
-                "squizlabs/php_codesniffer": "^3.8"
+                "squizlabs/php_codesniffer": "^3.11"
             },
             "suggest": {
                 "chillerlan/php-authenticator": "Yet another Google authenticator! Also creates URIs for mobile apps.",
@@ -142,19 +142,15 @@
             ],
             "support": {
                 "issues": "https://github.com/chillerlan/php-qrcode/issues",
-                "source": "https://github.com/chillerlan/php-qrcode/tree/4.4.1"
+                "source": "https://github.com/chillerlan/php-qrcode/tree/4.4.2"
             },
             "funding": [
-                {
-                    "url": "https://www.paypal.com/donate?hosted_button_id=WLYUNAT9ZTJZ4",
-                    "type": "custom"
-                },
                 {
                     "url": "https://ko-fi.com/codemasher",
                     "type": "ko_fi"
                 }
             ],
-            "time": "2024-01-06T16:56:58+00:00"
+            "time": "2024-11-15T15:36:24+00:00"
         },
         {
             "name": "chillerlan/php-settings-container",
@@ -418,11 +414,11 @@
         },
         {
             "name": "flashii/rpcii",
-            "version": "v2.0.0",
+            "version": "v2.0.1",
             "source": {
                 "type": "git",
                 "url": "https://patchii.net/flashii/rpcii-php.git",
-                "reference": "93ec139171d023f210f0b7464266b6fc42b4d838"
+                "reference": "1cbc1edb061612dc1d014a82e24b741d2a0bc11a"
             },
             "require": {
                 "ext-msgpack": ">=2.2",
@@ -453,15 +449,15 @@
             ],
             "description": "HTTP RPC client/server library.",
             "homepage": "https://railgun.sh/rpcii",
-            "time": "2024-11-13T23:17:29+00:00"
+            "time": "2024-11-14T02:22:09+00:00"
         },
         {
             "name": "flashwave/index",
-            "version": "v0.2410.191603",
+            "version": "v0.2410.211811",
             "source": {
                 "type": "git",
                 "url": "https://patchii.net/flash/index.git",
-                "reference": "17cdb4d1c239241200d7e30968122a8cd8b26509"
+                "reference": "40cbd35ba3855056987d2f7647f669e66f938979"
             },
             "require": {
                 "ext-mbstring": "*",
@@ -508,7 +504,7 @@
             ],
             "description": "Composer package for the common library for my projects.",
             "homepage": "https://railgun.sh/index",
-            "time": "2024-10-19T16:04:17+00:00"
+            "time": "2024-10-21T18:15:09+00:00"
         },
         {
             "name": "guzzlehttp/psr7",
@@ -628,28 +624,28 @@
         },
         {
             "name": "jean85/pretty-package-versions",
-            "version": "2.0.6",
+            "version": "2.1.0",
             "source": {
                 "type": "git",
                 "url": "https://github.com/Jean85/pretty-package-versions.git",
-                "reference": "f9fdd29ad8e6d024f52678b570e5593759b550b4"
+                "reference": "3c4e5f62ba8d7de1734312e4fff32f67a8daaf10"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/Jean85/pretty-package-versions/zipball/f9fdd29ad8e6d024f52678b570e5593759b550b4",
-                "reference": "f9fdd29ad8e6d024f52678b570e5593759b550b4",
+                "url": "https://api.github.com/repos/Jean85/pretty-package-versions/zipball/3c4e5f62ba8d7de1734312e4fff32f67a8daaf10",
+                "reference": "3c4e5f62ba8d7de1734312e4fff32f67a8daaf10",
                 "shasum": ""
             },
             "require": {
-                "composer-runtime-api": "^2.0.0",
-                "php": "^7.1|^8.0"
+                "composer-runtime-api": "^2.1.0",
+                "php": "^7.4|^8.0"
             },
             "require-dev": {
                 "friendsofphp/php-cs-fixer": "^3.2",
                 "jean85/composer-provided-replaced-stub-package": "^1.0",
                 "phpstan/phpstan": "^1.4",
-                "phpunit/phpunit": "^7.5|^8.5|^9.4",
-                "vimeo/psalm": "^4.3"
+                "phpunit/phpunit": "^7.5|^8.5|^9.6",
+                "vimeo/psalm": "^4.3 || ^5.0"
             },
             "type": "library",
             "extra": {
@@ -681,9 +677,9 @@
             ],
             "support": {
                 "issues": "https://github.com/Jean85/pretty-package-versions/issues",
-                "source": "https://github.com/Jean85/pretty-package-versions/tree/2.0.6"
+                "source": "https://github.com/Jean85/pretty-package-versions/tree/2.1.0"
             },
-            "time": "2024-03-08T09:58:59+00:00"
+            "time": "2024-11-18T16:19:46+00:00"
         },
         {
             "name": "matomo/device-detector",
@@ -1413,16 +1409,16 @@
         },
         {
             "name": "symfony/clock",
-            "version": "v7.1.6",
+            "version": "v7.2.0",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/clock.git",
-                "reference": "97bebc53548684c17ed696bc8af016880f0f098d"
+                "reference": "b81435fbd6648ea425d1ee96a2d8e68f4ceacd24"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/clock/zipball/97bebc53548684c17ed696bc8af016880f0f098d",
-                "reference": "97bebc53548684c17ed696bc8af016880f0f098d",
+                "url": "https://api.github.com/repos/symfony/clock/zipball/b81435fbd6648ea425d1ee96a2d8e68f4ceacd24",
+                "reference": "b81435fbd6648ea425d1ee96a2d8e68f4ceacd24",
                 "shasum": ""
             },
             "require": {
@@ -1467,7 +1463,7 @@
                 "time"
             ],
             "support": {
-                "source": "https://github.com/symfony/clock/tree/v7.1.6"
+                "source": "https://github.com/symfony/clock/tree/v7.2.0"
             },
             "funding": [
                 {
@@ -1483,20 +1479,20 @@
                     "type": "tidelift"
                 }
             ],
-            "time": "2024-09-25T14:20:29+00:00"
+            "time": "2024-09-25T14:21:43+00:00"
         },
         {
             "name": "symfony/deprecation-contracts",
-            "version": "v3.5.0",
+            "version": "v3.5.1",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/deprecation-contracts.git",
-                "reference": "0e0d29ce1f20deffb4ab1b016a7257c4f1e789a1"
+                "reference": "74c71c939a79f7d5bf3c1ce9f5ea37ba0114c6f6"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/0e0d29ce1f20deffb4ab1b016a7257c4f1e789a1",
-                "reference": "0e0d29ce1f20deffb4ab1b016a7257c4f1e789a1",
+                "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/74c71c939a79f7d5bf3c1ce9f5ea37ba0114c6f6",
+                "reference": "74c71c939a79f7d5bf3c1ce9f5ea37ba0114c6f6",
                 "shasum": ""
             },
             "require": {
@@ -1534,7 +1530,7 @@
             "description": "A generic function and convention to trigger deprecation notices",
             "homepage": "https://symfony.com",
             "support": {
-                "source": "https://github.com/symfony/deprecation-contracts/tree/v3.5.0"
+                "source": "https://github.com/symfony/deprecation-contracts/tree/v3.5.1"
             },
             "funding": [
                 {
@@ -1550,20 +1546,20 @@
                     "type": "tidelift"
                 }
             ],
-            "time": "2024-04-18T09:32:20+00:00"
+            "time": "2024-09-25T14:20:29+00:00"
         },
         {
             "name": "symfony/event-dispatcher",
-            "version": "v7.1.6",
+            "version": "v7.2.0",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/event-dispatcher.git",
-                "reference": "87254c78dd50721cfd015b62277a8281c5589702"
+                "reference": "910c5db85a5356d0fea57680defec4e99eb9c8c1"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/87254c78dd50721cfd015b62277a8281c5589702",
-                "reference": "87254c78dd50721cfd015b62277a8281c5589702",
+                "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/910c5db85a5356d0fea57680defec4e99eb9c8c1",
+                "reference": "910c5db85a5356d0fea57680defec4e99eb9c8c1",
                 "shasum": ""
             },
             "require": {
@@ -1614,7 +1610,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.1.6"
+                "source": "https://github.com/symfony/event-dispatcher/tree/v7.2.0"
             },
             "funding": [
                 {
@@ -1630,20 +1626,20 @@
                     "type": "tidelift"
                 }
             ],
-            "time": "2024-09-25T14:20:29+00:00"
+            "time": "2024-09-25T14:21:43+00:00"
         },
         {
             "name": "symfony/event-dispatcher-contracts",
-            "version": "v3.5.0",
+            "version": "v3.5.1",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/event-dispatcher-contracts.git",
-                "reference": "8f93aec25d41b72493c6ddff14e916177c9efc50"
+                "reference": "7642f5e970b672283b7823222ae8ef8bbc160b9f"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/event-dispatcher-contracts/zipball/8f93aec25d41b72493c6ddff14e916177c9efc50",
-                "reference": "8f93aec25d41b72493c6ddff14e916177c9efc50",
+                "url": "https://api.github.com/repos/symfony/event-dispatcher-contracts/zipball/7642f5e970b672283b7823222ae8ef8bbc160b9f",
+                "reference": "7642f5e970b672283b7823222ae8ef8bbc160b9f",
                 "shasum": ""
             },
             "require": {
@@ -1690,7 +1686,7 @@
                 "standards"
             ],
             "support": {
-                "source": "https://github.com/symfony/event-dispatcher-contracts/tree/v3.5.0"
+                "source": "https://github.com/symfony/event-dispatcher-contracts/tree/v3.5.1"
             },
             "funding": [
                 {
@@ -1706,7 +1702,7 @@
                     "type": "tidelift"
                 }
             ],
-            "time": "2024-04-18T09:32:20+00:00"
+            "time": "2024-09-25T14:20:29+00:00"
         },
         {
             "name": "symfony/mailer",
@@ -1790,16 +1786,16 @@
         },
         {
             "name": "symfony/mime",
-            "version": "v7.1.6",
+            "version": "v7.2.0",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/mime.git",
-                "reference": "caa1e521edb2650b8470918dfe51708c237f0598"
+                "reference": "cc84a4b81f62158c3846ac7ff10f696aae2b524d"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/mime/zipball/caa1e521edb2650b8470918dfe51708c237f0598",
-                "reference": "caa1e521edb2650b8470918dfe51708c237f0598",
+                "url": "https://api.github.com/repos/symfony/mime/zipball/cc84a4b81f62158c3846ac7ff10f696aae2b524d",
+                "reference": "cc84a4b81f62158c3846ac7ff10f696aae2b524d",
                 "shasum": ""
             },
             "require": {
@@ -1854,7 +1850,7 @@
                 "mime-type"
             ],
             "support": {
-                "source": "https://github.com/symfony/mime/tree/v7.1.6"
+                "source": "https://github.com/symfony/mime/tree/v7.2.0"
             },
             "funding": [
                 {
@@ -1870,20 +1866,20 @@
                     "type": "tidelift"
                 }
             ],
-            "time": "2024-10-25T15:11:02+00:00"
+            "time": "2024-11-23T09:19:39+00:00"
         },
         {
             "name": "symfony/options-resolver",
-            "version": "v7.1.6",
+            "version": "v7.2.0",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/options-resolver.git",
-                "reference": "85e95eeede2d41cd146146e98c9c81d9214cae85"
+                "reference": "7da8fbac9dcfef75ffc212235d76b2754ce0cf50"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/options-resolver/zipball/85e95eeede2d41cd146146e98c9c81d9214cae85",
-                "reference": "85e95eeede2d41cd146146e98c9c81d9214cae85",
+                "url": "https://api.github.com/repos/symfony/options-resolver/zipball/7da8fbac9dcfef75ffc212235d76b2754ce0cf50",
+                "reference": "7da8fbac9dcfef75ffc212235d76b2754ce0cf50",
                 "shasum": ""
             },
             "require": {
@@ -1921,7 +1917,7 @@
                 "options"
             ],
             "support": {
-                "source": "https://github.com/symfony/options-resolver/tree/v7.1.6"
+                "source": "https://github.com/symfony/options-resolver/tree/v7.2.0"
             },
             "funding": [
                 {
@@ -1937,7 +1933,7 @@
                     "type": "tidelift"
                 }
             ],
-            "time": "2024-09-25T14:20:29+00:00"
+            "time": "2024-11-20T11:17:29+00:00"
         },
         {
             "name": "symfony/polyfill-ctype",
@@ -2416,16 +2412,16 @@
         },
         {
             "name": "symfony/service-contracts",
-            "version": "v3.5.0",
+            "version": "v3.5.1",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/service-contracts.git",
-                "reference": "bd1d9e59a81d8fa4acdcea3f617c581f7475a80f"
+                "reference": "e53260aabf78fb3d63f8d79d69ece59f80d5eda0"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/service-contracts/zipball/bd1d9e59a81d8fa4acdcea3f617c581f7475a80f",
-                "reference": "bd1d9e59a81d8fa4acdcea3f617c581f7475a80f",
+                "url": "https://api.github.com/repos/symfony/service-contracts/zipball/e53260aabf78fb3d63f8d79d69ece59f80d5eda0",
+                "reference": "e53260aabf78fb3d63f8d79d69ece59f80d5eda0",
                 "shasum": ""
             },
             "require": {
@@ -2479,7 +2475,7 @@
                 "standards"
             ],
             "support": {
-                "source": "https://github.com/symfony/service-contracts/tree/v3.5.0"
+                "source": "https://github.com/symfony/service-contracts/tree/v3.5.1"
             },
             "funding": [
                 {
@@ -2495,24 +2491,25 @@
                     "type": "tidelift"
                 }
             ],
-            "time": "2024-04-18T09:32:20+00:00"
+            "time": "2024-09-25T14:20:29+00:00"
         },
         {
             "name": "symfony/translation",
-            "version": "v7.1.6",
+            "version": "v7.2.0",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/translation.git",
-                "reference": "b9f72ab14efdb6b772f85041fa12f820dee8d55f"
+                "reference": "dc89e16b44048ceecc879054e5b7f38326ab6cc5"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/translation/zipball/b9f72ab14efdb6b772f85041fa12f820dee8d55f",
-                "reference": "b9f72ab14efdb6b772f85041fa12f820dee8d55f",
+                "url": "https://api.github.com/repos/symfony/translation/zipball/dc89e16b44048ceecc879054e5b7f38326ab6cc5",
+                "reference": "dc89e16b44048ceecc879054e5b7f38326ab6cc5",
                 "shasum": ""
             },
             "require": {
                 "php": ">=8.2",
+                "symfony/deprecation-contracts": "^2.5|^3",
                 "symfony/polyfill-mbstring": "~1.0",
                 "symfony/translation-contracts": "^2.5|^3.0"
             },
@@ -2573,7 +2570,7 @@
             "description": "Provides tools to internationalize your application",
             "homepage": "https://symfony.com",
             "support": {
-                "source": "https://github.com/symfony/translation/tree/v7.1.6"
+                "source": "https://github.com/symfony/translation/tree/v7.2.0"
             },
             "funding": [
                 {
@@ -2589,20 +2586,20 @@
                     "type": "tidelift"
                 }
             ],
-            "time": "2024-09-28T12:35:13+00:00"
+            "time": "2024-11-12T20:47:56+00:00"
         },
         {
             "name": "symfony/translation-contracts",
-            "version": "v3.5.0",
+            "version": "v3.5.1",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/translation-contracts.git",
-                "reference": "b9d2189887bb6b2e0367a9fc7136c5239ab9b05a"
+                "reference": "4667ff3bd513750603a09c8dedbea942487fb07c"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/translation-contracts/zipball/b9d2189887bb6b2e0367a9fc7136c5239ab9b05a",
-                "reference": "b9d2189887bb6b2e0367a9fc7136c5239ab9b05a",
+                "url": "https://api.github.com/repos/symfony/translation-contracts/zipball/4667ff3bd513750603a09c8dedbea942487fb07c",
+                "reference": "4667ff3bd513750603a09c8dedbea942487fb07c",
                 "shasum": ""
             },
             "require": {
@@ -2651,7 +2648,7 @@
                 "standards"
             ],
             "support": {
-                "source": "https://github.com/symfony/translation-contracts/tree/v3.5.0"
+                "source": "https://github.com/symfony/translation-contracts/tree/v3.5.1"
             },
             "funding": [
                 {
@@ -2667,20 +2664,20 @@
                     "type": "tidelift"
                 }
             ],
-            "time": "2024-04-18T09:32:20+00:00"
+            "time": "2024-09-25T14:20:29+00:00"
         },
         {
             "name": "twig/html-extra",
-            "version": "v3.13.0",
+            "version": "v3.16.0",
             "source": {
                 "type": "git",
                 "url": "https://github.com/twigphp/html-extra.git",
-                "reference": "8229e750091171c1f11801a525927811c7ac5a7e"
+                "reference": "2086023d3ffc4bae2b1115f715d17f97fd013665"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/twigphp/html-extra/zipball/8229e750091171c1f11801a525927811c7ac5a7e",
-                "reference": "8229e750091171c1f11801a525927811c7ac5a7e",
+                "url": "https://api.github.com/repos/twigphp/html-extra/zipball/2086023d3ffc4bae2b1115f715d17f97fd013665",
+                "reference": "2086023d3ffc4bae2b1115f715d17f97fd013665",
                 "shasum": ""
             },
             "require": {
@@ -2723,7 +2720,7 @@
                 "twig"
             ],
             "support": {
-                "source": "https://github.com/twigphp/html-extra/tree/v3.13.0"
+                "source": "https://github.com/twigphp/html-extra/tree/v3.16.0"
             },
             "funding": [
                 {
@@ -2735,20 +2732,20 @@
                     "type": "tidelift"
                 }
             ],
-            "time": "2024-09-03T13:08:40+00:00"
+            "time": "2024-09-30T06:41:48+00:00"
         },
         {
             "name": "twig/twig",
-            "version": "v3.14.2",
+            "version": "v3.16.0",
             "source": {
                 "type": "git",
                 "url": "https://github.com/twigphp/Twig.git",
-                "reference": "0b6f9d8370bb3b7f1ce5313ed8feb0fafd6e399a"
+                "reference": "475ad2dc97d65d8631393e721e7e44fb544f0561"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/twigphp/Twig/zipball/0b6f9d8370bb3b7f1ce5313ed8feb0fafd6e399a",
-                "reference": "0b6f9d8370bb3b7f1ce5313ed8feb0fafd6e399a",
+                "url": "https://api.github.com/repos/twigphp/Twig/zipball/475ad2dc97d65d8631393e721e7e44fb544f0561",
+                "reference": "475ad2dc97d65d8631393e721e7e44fb544f0561",
                 "shasum": ""
             },
             "require": {
@@ -2759,6 +2756,7 @@
                 "symfony/polyfill-php81": "^1.29"
             },
             "require-dev": {
+                "phpstan/phpstan": "^2.0",
                 "psr/container": "^1.0|^2.0",
                 "symfony/phpunit-bridge": "^5.4.9|^6.4|^7.0"
             },
@@ -2802,7 +2800,7 @@
             ],
             "support": {
                 "issues": "https://github.com/twigphp/Twig/issues",
-                "source": "https://github.com/twigphp/Twig/tree/v3.14.2"
+                "source": "https://github.com/twigphp/Twig/tree/v3.16.0"
             },
             "funding": [
                 {
@@ -2814,22 +2812,22 @@
                     "type": "tidelift"
                 }
             ],
-            "time": "2024-11-07T12:36:22+00:00"
+            "time": "2024-11-29T08:27:05+00:00"
         }
     ],
     "packages-dev": [
         {
             "name": "phpstan/phpstan",
-            "version": "1.12.10",
+            "version": "1.12.12",
             "source": {
                 "type": "git",
                 "url": "https://github.com/phpstan/phpstan.git",
-                "reference": "fc463b5d0fe906dcf19689be692c65c50406a071"
+                "reference": "b5ae1b88f471d3fd4ba1aa0046234b5ca3776dd0"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/phpstan/phpstan/zipball/fc463b5d0fe906dcf19689be692c65c50406a071",
-                "reference": "fc463b5d0fe906dcf19689be692c65c50406a071",
+                "url": "https://api.github.com/repos/phpstan/phpstan/zipball/b5ae1b88f471d3fd4ba1aa0046234b5ca3776dd0",
+                "reference": "b5ae1b88f471d3fd4ba1aa0046234b5ca3776dd0",
                 "shasum": ""
             },
             "require": {
@@ -2874,15 +2872,17 @@
                     "type": "github"
                 }
             ],
-            "time": "2024-11-11T15:37:09+00:00"
+            "time": "2024-11-28T22:13:23+00:00"
         }
     ],
     "aliases": [],
     "minimum-stability": "stable",
-    "stability-flags": [],
+    "stability-flags": {},
     "prefer-stable": false,
     "prefer-lowest": false,
-    "platform": [],
-    "platform-dev": [],
+    "platform": {
+        "php": ">=8.4"
+    },
+    "platform-dev": {},
     "plugin-api-version": "2.6.0"
 }
diff --git a/public-legacy/_github-callback.php b/public-legacy/_github-callback.php
index 096af9c1..aaa7d0a3 100644
--- a/public-legacy/_github-callback.php
+++ b/public-legacy/_github-callback.php
@@ -101,8 +101,6 @@ if(!empty($repoInfo['master']))
 if($data->ref !== $repoMaster)
     die('only the master branch is tracked');
 
-$changelog = $msz->getChangelog();
-
 $tags = $repoInfo['tags'] ?? [];
 $addresses = $config['addresses'] ?? [];
 
@@ -118,7 +116,7 @@ foreach($data->commits as $commit) {
     $line = $index === false ? $message : mb_substr($message, 0, $index);
     $body = trim($index === false ? '' : mb_substr($message, $index + 1));
 
-    $changeInfo = $changelog->createChange(
+    $changeInfo = $msz->changelog->createChange(
         ghcb_changelog_action($line),
         $line, $body,
         $addresses[$commit->author->email] ?? null,
@@ -127,5 +125,5 @@ foreach($data->commits as $commit) {
 
     if(!empty($tags))
         foreach($tags as $tag)
-            $changelog->addTagToChange($changeInfo, $tag);
+            $msz->changelog->addTagToChange($changeInfo, $tag);
 }
diff --git a/public-legacy/auth/login.php b/public-legacy/auth/login.php
index 33083707..6b408e83 100644
--- a/public-legacy/auth/login.php
+++ b/public-legacy/auth/login.php
@@ -4,37 +4,30 @@ namespace Misuzu;
 use Exception;
 use Misuzu\Auth\AuthTokenCookie;
 
-$urls = $msz->getUrls();
-$authInfo = $msz->getAuthInfo();
-if($authInfo->isLoggedIn()) {
-    Tools::redirect($urls->format('index'));
+if($msz->authInfo->isLoggedIn) {
+    Tools::redirect($msz->urls->format('index'));
     return;
 }
 
-$authCtx = $msz->getAuthContext();
-$users = $msz->getUsersContext()->getUsers();
-$sessions = $authCtx->getSessions();
-$loginAttempts = $authCtx->getLoginAttempts();
-
 if(!empty($_GET['resolve'])) {
     header('Content-Type: application/json; charset=utf-8');
 
     try {
         // Only works for usernames, this is by design
-        $userInfo = $users->getUser((string)filter_input(INPUT_GET, 'name'), 'name');
+        $userInfo = $msz->usersCtx->users->getUser((string)filter_input(INPUT_GET, 'name'), 'name');
     } catch(Exception $ex) {
         echo json_encode([
             'id' => 0,
             'name' => '',
-            'avatar' => $urls->format('user-avatar', ['res' => 200, 'user' => 0]),
+            'avatar' => $msz->urls->format('user-avatar', ['res' => 200, 'user' => 0]),
         ]);
         return;
     }
 
     echo json_encode([
         'id' => (int)$userInfo->getId(),
-        'name' => $userInfo->getName(),
-        'avatar' => $urls->format('user-avatar', ['user' => $userInfo->getId(), 'res' => 200]),
+        'name' => $userInfo->name,
+        'avatar' => $msz->urls->format('user-avatar', ['user' => $userInfo->getId(), 'res' => 200]),
     ]);
     return;
 }
@@ -44,7 +37,7 @@ $ipAddress = $_SERVER['REMOTE_ADDR'];
 $countryCode = $_SERVER['COUNTRY_CODE'] ?? 'XX';
 $userAgent = $_SERVER['HTTP_USER_AGENT'] ?? '';
 
-$remainingAttempts = $loginAttempts->countRemainingAttempts($ipAddress);
+$remainingAttempts = $msz->authCtx->loginAttempts->countRemainingAttempts($ipAddress);
 
 $siteIsPrivate = $cfg->getBoolean('private.enable');
 if($siteIsPrivate) {
@@ -95,49 +88,49 @@ while(!empty($_POST['login']) && is_array($_POST['login'])) {
     $loginFailedError = "Invalid username or password, {$attemptsRemainingError}.";
 
     try {
-        $userInfo = $users->getUser($_POST['login']['username'], 'login');
+        $userInfo = $msz->usersCtx->users->getUser($_POST['login']['username'], 'login');
     } catch(Exception $ex) {
-        $loginAttempts->recordAttempt(false, $ipAddress, $countryCode, $userAgent, $clientInfo);
+        $msz->authCtx->loginAttempts->recordAttempt(false, $ipAddress, $countryCode, $userAgent, $clientInfo);
         $notices[] = $loginFailedError;
         break;
     }
 
-    if(!$userInfo->hasPasswordHash()) {
+    if(!$userInfo->hasPasswordHash) {
         $notices[] = 'Your password has been invalidated, please reset it.';
         break;
     }
 
-    if($userInfo->isDeleted() || !$userInfo->verifyPassword($_POST['login']['password'])) {
-        $loginAttempts->recordAttempt(false, $ipAddress, $countryCode, $userAgent, $clientInfo, $userInfo);
+    if($userInfo->deleted || !$userInfo->verifyPassword($_POST['login']['password'])) {
+        $msz->authCtx->loginAttempts->recordAttempt(false, $ipAddress, $countryCode, $userAgent, $clientInfo, $userInfo);
         $notices[] = $loginFailedError;
         break;
     }
 
-    if($userInfo->passwordNeedsRehash())
-        $users->updateUser($userInfo, password: $_POST['login']['password']);
+    if($userInfo->passwordNeedsRehash)
+        $msz->usersCtx->users->updateUser($userInfo, password: $_POST['login']['password']);
 
-    if(!empty($loginPermCat) && $loginPermVal > 0 && !$msz->getPerms()->checkPermissions($loginPermCat, $loginPermVal, $userInfo)) {
+    if(!empty($loginPermCat) && $loginPermVal > 0 && !$msz->perms->checkPermissions($loginPermCat, $loginPermVal, $userInfo)) {
         $notices[] = "Login succeeded, but you're not allowed to browse the site right now.";
-        $loginAttempts->recordAttempt(true, $ipAddress, $countryCode, $userAgent, $clientInfo, $userInfo);
+        $msz->authCtx->loginAttempts->recordAttempt(true, $ipAddress, $countryCode, $userAgent, $clientInfo, $userInfo);
         break;
     }
 
-    if($userInfo->hasTOTPKey()) {
-        $tfaToken = $authCtx->getTwoFactorAuthSessions()->createToken($userInfo);
-        Tools::redirect($urls->format('auth-two-factor', ['token' => $tfaToken]));
+    if($userInfo->hasTOTP) {
+        $tfaToken = $msz->authCtx->tfaSessions->createToken($userInfo);
+        Tools::redirect($msz->urls->format('auth-two-factor', ['token' => $tfaToken]));
         return;
     }
 
-    $loginAttempts->recordAttempt(true, $ipAddress, $countryCode, $userAgent, $clientInfo, $userInfo);
+    $msz->authCtx->loginAttempts->recordAttempt(true, $ipAddress, $countryCode, $userAgent, $clientInfo, $userInfo);
 
     try {
-        $sessionInfo = $sessions->createSession($userInfo, $ipAddress, $countryCode, $userAgent, $clientInfo);
+        $sessionInfo = $msz->authCtx->sessions->createSession($userInfo, $ipAddress, $countryCode, $userAgent, $clientInfo);
     } catch(Exception $ex) {
         $notices[] = "Something broke while creating a session for you, please tell an administrator or developer about this!";
         break;
     }
 
-    $tokenBuilder = $authInfo->getTokenInfo()->toBuilder();
+    $tokenBuilder = $msz->authInfo->tokenInfo->toBuilder();
     $tokenBuilder->setUserId($userInfo);
     $tokenBuilder->setSessionToken($sessionInfo);
     $tokenBuilder->removeImpersonatedUserId();
@@ -146,7 +139,7 @@ while(!empty($_POST['login']) && is_array($_POST['login'])) {
     AuthTokenCookie::apply($tokenPacker->pack($tokenInfo));
 
     if(!Tools::isLocalURL($loginRedirect))
-        $loginRedirect = $urls->format('index');
+        $loginRedirect = $msz->urls->format('index');
 
     Tools::redirect($loginRedirect);
     return;
@@ -156,7 +149,7 @@ $welcomeMode = !empty($_GET['welcome']);
 $loginUsername = !empty($_POST['login']['username']) && is_string($_POST['login']['username']) ? $_POST['login']['username'] : (
     !empty($_GET['username']) && is_string($_GET['username']) ? $_GET['username'] : ''
 );
-$loginRedirect = $welcomeMode ? $urls->format('index') : (!empty($_GET['redirect']) && is_string($_GET['redirect']) ? $_GET['redirect'] : null) ?? $_SERVER['HTTP_REFERER'] ?? $urls->format('index');
+$loginRedirect = $welcomeMode ? $msz->urls->format('index') : (!empty($_GET['redirect']) && is_string($_GET['redirect']) ? $_GET['redirect'] : null) ?? $_SERVER['HTTP_REFERER'] ?? $msz->urls->format('index');
 $canRegisterAccount = !$siteIsPrivate;
 
 Template::render('auth.login', [
diff --git a/public-legacy/auth/logout.php b/public-legacy/auth/logout.php
index 1f9eb732..57905f8c 100644
--- a/public-legacy/auth/logout.php
+++ b/public-legacy/auth/logout.php
@@ -3,25 +3,22 @@ namespace Misuzu;
 
 use Misuzu\Auth\AuthTokenCookie;
 
-$authInfo = $msz->getAuthInfo();
-if($authInfo->isLoggedIn()) {
+if($msz->authInfo->isLoggedIn) {
     if(!CSRF::validateRequest()) {
         Template::render('auth.logout');
         return;
     }
 
-    $tokenInfo = $authInfo->getTokenInfo();
-
-    $authCtx = $msz->getAuthContext();
-    $authCtx->getSessions()->deleteSessions(sessionTokens: $tokenInfo->getSessionToken());
+    $tokenInfo = $msz->authInfo->tokenInfo;
+    $msz->authCtx->sessions->deleteSessions(sessionTokens: $tokenInfo->sessionToken);
 
     $tokenBuilder = $tokenInfo->toBuilder();
     $tokenBuilder->removeUserId();
     $tokenBuilder->removeSessionToken();
     $tokenBuilder->removeImpersonatedUserId();
-    $tokenInfo = $tokenBuilder->toInfo();
 
+    $tokenInfo = $tokenBuilder->toInfo();
     AuthTokenCookie::apply($tokenPacker->pack($tokenInfo));
 }
 
-Tools::redirect($msz->getUrls()->format('index'));;
+Tools::redirect($msz->urls->format('index'));;
diff --git a/public-legacy/auth/password.php b/public-legacy/auth/password.php
index 5f699eca..86a49647 100644
--- a/public-legacy/auth/password.php
+++ b/public-legacy/auth/password.php
@@ -4,18 +4,11 @@ namespace Misuzu;
 use RuntimeException;
 use Misuzu\Users\User;
 
-$urls = $msz->getUrls();
-$authInfo = $msz->getAuthInfo();
-if($authInfo->isLoggedIn()) {
-    Tools::redirect($urls->format('settings-account'));
+if($msz->authInfo->isLoggedIn) {
+    Tools::redirect($msz->urls->format('settings-account'));
     return;
 }
 
-$authCtx = $msz->getAuthContext();
-$users = $msz->getUsersContext()->getUsers();
-$recoveryTokens = $authCtx->getRecoveryTokens();
-$loginAttempts = $authCtx->getLoginAttempts();
-
 $reset = !empty($_POST['reset']) && is_array($_POST['reset']) ? $_POST['reset'] : [];
 $forgot = !empty($_POST['forgot']) && is_array($_POST['forgot']) ? $_POST['forgot'] : [];
 $userId = !empty($reset['user']) ? (int)$reset['user'] : (
@@ -24,9 +17,9 @@ $userId = !empty($reset['user']) ? (int)$reset['user'] : (
 
 if($userId > 0)
     try {
-        $userInfo = $users->getUser((string)$userId, 'id');
+        $userInfo = $msz->usersCtx->users->getUser((string)$userId, 'id');
     } catch(RuntimeException $ex) {
-        Tools::redirect($urls->format('auth-forgot'));
+        Tools::redirect($msz->urls->format('auth-forgot'));
         return;
     }
 
@@ -35,7 +28,7 @@ $ipAddress = $_SERVER['REMOTE_ADDR'];
 $siteIsPrivate = $cfg->getBoolean('private.enable');
 $canResetPassword = $siteIsPrivate ? $cfg->getBoolean('private.allow_password_reset', true) : true;
 
-$remainingAttempts = $loginAttempts->countRemainingAttempts($ipAddress);
+$remainingAttempts = $msz->authCtx->loginAttempts->countRemainingAttempts($ipAddress);
 
 while($canResetPassword) {
     if(!empty($reset) && $userId > 0) {
@@ -47,12 +40,12 @@ while($canResetPassword) {
         $verifyCode = !empty($reset['verification']) && is_string($reset['verification']) ? $reset['verification'] : '';
 
         try {
-            $tokenInfo = $recoveryTokens->getToken(verifyCode: $verifyCode);
+            $tokenInfo = $msz->authCtx->recoveryTokens->getToken(verifyCode: $verifyCode);
         } catch(RuntimeException $ex) {
             unset($tokenInfo);
         }
 
-        if(empty($tokenInfo) || !$tokenInfo->isValid() || $tokenInfo->getUserId() !== (string)$userInfo->getId()) {
+        if(empty($tokenInfo) || !$tokenInfo->isValid || $tokenInfo->userId !== (string)$userInfo->getId()) {
             $notices[] = 'Invalid verification code!';
             break;
         }
@@ -67,21 +60,21 @@ while($canResetPassword) {
             break;
         }
 
-        $passwordValidation = $users->validatePassword($passwordNew);
+        $passwordValidation = $msz->usersCtx->users->validatePassword($passwordNew);
         if($passwordValidation !== '') {
-            $notices[] = $users->validatePasswordText($passwordValidation);
+            $notices[] = $msz->usersCtx->users->validatePasswordText($passwordValidation);
             break;
         }
 
         // also disables two factor auth to prevent getting locked out of account entirely
         // this behaviour should really be replaced with recovery keys...
-        $users->updateUser($userInfo, password: $passwordNew, totpKey: '');
+        $msz->usersCtx->users->updateUser($userInfo, password: $passwordNew, totpKey: '');
 
         $msz->createAuditLog('PASSWORD_RESET', [], $userInfo);
 
-        $recoveryTokens->invalidateToken($tokenInfo);
+        $msz->authCtx->recoveryTokens->invalidateToken($tokenInfo);
 
-        Tools::redirect($urls->format('auth-login', ['redirect' => '/']));
+        Tools::redirect($msz->urls->format('auth-login', ['redirect' => '/']));
         return;
     }
 
@@ -102,39 +95,39 @@ while($canResetPassword) {
         }
 
         try {
-            $forgotUser = $users->getUser($forgot['email'], 'email');
+            $forgotUser = $msz->usersCtx->users->getUser($forgot['email'], 'email');
         } catch(RuntimeException $ex) {
             unset($forgotUser);
         }
 
-        if(empty($forgotUser) || $forgotUser->isDeleted()) {
+        if(empty($forgotUser) || $forgotUser->deleted) {
             $notices[] = "This e-mail address is not registered with us.";
             break;
         }
 
         try {
-            $tokenInfo = $recoveryTokens->getToken(userInfo: $forgotUser, remoteAddr: $ipAddress);
+            $tokenInfo = $msz->authCtx->recoveryTokens->getToken(userInfo: $forgotUser, remoteAddr: $ipAddress);
         } catch(RuntimeException $ex) {
-            $tokenInfo = $recoveryTokens->createToken($forgotUser, $ipAddress);
+            $tokenInfo = $msz->authCtx->recoveryTokens->createToken($forgotUser, $ipAddress);
 
             $recoveryMessage = Mailer::template('password-recovery', [
-                'username' => $forgotUser->getName(),
-                'token' => $tokenInfo->getCode(),
+                'username' => $forgotUser->name,
+                'token' => $tokenInfo->code,
             ]);
 
             $recoveryMail = Mailer::sendMessage(
-                [$forgotUser->getEMailAddress() => $forgotUser->getName()],
+                [$forgotUser->emailAddress => $forgotUser->name],
                 $recoveryMessage['subject'], $recoveryMessage['message']
             );
 
             if(!$recoveryMail) {
                 $notices[] = "Failed to send reset email, please contact the administrator.";
-                $recoveryTokens->invalidateToken($tokenInfo);
+                $msz->authCtx->recoveryTokens->invalidateToken($tokenInfo);
                 break;
             }
         }
 
-        Tools::redirect($urls->format('auth-reset', ['user' => $forgotUser->getId()]));
+        Tools::redirect($msz->urls->format('auth-reset', ['user' => $forgotUser->getId()]));
         return;
     }
 
diff --git a/public-legacy/auth/register.php b/public-legacy/auth/register.php
index 59e6b7a4..3d1219a5 100644
--- a/public-legacy/auth/register.php
+++ b/public-legacy/auth/register.php
@@ -4,19 +4,11 @@ namespace Misuzu;
 use RuntimeException;
 use Misuzu\Users\User;
 
-$urls = $msz->getUrls();
-$authInfo = $msz->getAuthInfo();
-if($authInfo->isLoggedIn()) {
-    Tools::redirect($urls->format('index'));
+if($msz->authInfo->isLoggedIn) {
+    Tools::redirect($msz->urls->format('index'));
     return;
 }
 
-$authCtx = $msz->getAuthContext();
-$usersCtx = $msz->getUsersContext();
-$users = $usersCtx->getUsers();
-$roles = $usersCtx->getRoles();
-$config = $msz->getConfig();
-
 $register = !empty($_POST['register']) && is_array($_POST['register']) ? $_POST['register'] : [];
 $notices = [];
 $ipAddress = $_SERVER['REMOTE_ADDR'];
@@ -33,8 +25,7 @@ $countryCode = $_SERVER['COUNTRY_CODE'] ?? 'XX';
 //  for fast matching
 $restricted = '';
 
-$loginAttempts = $authCtx->getLoginAttempts();
-$remainingAttempts = $loginAttempts->countRemainingAttempts($ipAddress);
+$remainingAttempts = $msz->authCtx->loginAttempts->countRemainingAttempts($ipAddress);
 
 while(!$restricted && !empty($register)) {
     if(!CSRF::validateRequest()) {
@@ -69,28 +60,28 @@ while(!$restricted && !empty($register)) {
         break;
     }
 
-    $usernameValidation = $users->validateName($register['username']);
+    $usernameValidation = $msz->usersCtx->users->validateName($register['username']);
     if($usernameValidation !== '')
-        $notices[] = $users->validateNameText($usernameValidation);
+        $notices[] = $msz->usersCtx->users->validateNameText($usernameValidation);
 
-    $emailValidation = $users->validateEMailAddress($register['email']);
+    $emailValidation = $msz->usersCtx->users->validateEMailAddress($register['email']);
     if($emailValidation !== '')
-        $notices[] = $users->validateEMailAddressText($emailValidation);
+        $notices[] = $msz->usersCtx->users->validateEMailAddressText($emailValidation);
 
     if($register['password_confirm'] !== $register['password'])
         $notices[] = 'The given passwords don\'t match.';
 
-    $passwordValidation = $users->validatePassword($register['password']);
+    $passwordValidation = $msz->usersCtx->users->validatePassword($register['password']);
     if($passwordValidation !== '')
-        $notices[] = $users->validatePasswordText($passwordValidation);
+        $notices[] = $msz->usersCtx->users->validatePasswordText($passwordValidation);
 
     if(!empty($notices))
         break;
 
-    $defaultRoleInfo = $roles->getDefaultRole();
+    $defaultRoleInfo = $msz->usersCtx->roles->getDefaultRole();
 
     try {
-        $userInfo = $users->createUser(
+        $userInfo = $msz->usersCtx->users->createUser(
             $register['username'],
             $register['password'],
             $register['email'],
@@ -103,14 +94,14 @@ while(!$restricted && !empty($register)) {
         break;
     }
 
-    $users->addRoles($userInfo, $defaultRoleInfo);
-    $config->setString('users.newest', $userInfo->getId());
-    $msz->getPerms()->precalculatePermissions(
-        $msz->getForumContext()->getCategories(),
+    $msz->usersCtx->users->addRoles($userInfo, $defaultRoleInfo);
+    $msz->config->setString('users.newest', $userInfo->getId());
+    $msz->perms->precalculatePermissions(
+        $msz->forumCtx->categories,
         [$userInfo->getId()]
     );
 
-    Tools::redirect($urls->format('auth-login-welcome', ['username' => $userInfo->getName()]));
+    Tools::redirect($msz->urls->format('auth-login-welcome', ['username' => $userInfo->name]));
     return;
 }
 
diff --git a/public-legacy/auth/revert.php b/public-legacy/auth/revert.php
index e215be71..1608624e 100644
--- a/public-legacy/auth/revert.php
+++ b/public-legacy/auth/revert.php
@@ -3,21 +3,20 @@ namespace Misuzu;
 
 use Misuzu\Auth\AuthTokenCookie;
 
-$urls = $msz->getUrls();
 if(CSRF::validateRequest()) {
-    $tokenInfo = $msz->getAuthInfo()->getTokenInfo();
+    $tokenInfo = $msz->authInfo->tokenInfo;
 
-    if($tokenInfo->hasImpersonatedUserId()) {
-        $impUserId = $tokenInfo->getImpersonatedUserId();
+    if($tokenInfo->hasImpersonatedUserId) {
+        $impUserId = $tokenInfo->impersonatedUserId;
 
         $tokenBuilder = $tokenInfo->toBuilder();
         $tokenBuilder->removeImpersonatedUserId();
-        $tokenInfo = $tokenBuilder->toInfo();
 
+        $tokenInfo = $tokenBuilder->toInfo();
         AuthTokenCookie::apply($tokenPacker->pack($tokenInfo));
-        Tools::redirect($urls->format('manage-user', ['user' => $impUserId]));
+        Tools::redirect($msz->urls->format('manage-user', ['user' => $impUserId]));
         return;
     }
 }
 
-Tools::redirect($urls->format('index'));
+Tools::redirect($msz->urls->format('index'));
diff --git a/public-legacy/auth/twofactor.php b/public-legacy/auth/twofactor.php
index d3e08e37..55f6636a 100644
--- a/public-legacy/auth/twofactor.php
+++ b/public-legacy/auth/twofactor.php
@@ -5,43 +5,35 @@ use RuntimeException;
 use Misuzu\TOTPGenerator;
 use Misuzu\Auth\AuthTokenCookie;
 
-$urls = $msz->getUrls();
-$authInfo = $msz->getAuthInfo();
-if($authInfo->isLoggedIn()) {
-    Tools::redirect($urls->format('index'));
+if($msz->authInfo->isLoggedIn) {
+    Tools::redirect($msz->urls->format('index'));
     return;
 }
 
-$authCtx = $msz->getAuthContext();
-$users = $msz->getUsersContext()->getUsers();
-$sessions = $authCtx->getSessions();
-$tfaSessions = $authCtx->getTwoFactorAuthSessions();
-$loginAttempts = $authCtx->getLoginAttempts();
-
 $ipAddress = $_SERVER['REMOTE_ADDR'];
 $countryCode = $_SERVER['COUNTRY_CODE'] ?? 'XX';
 $userAgent = $_SERVER['HTTP_USER_AGENT'] ?? '';
 $twofactor = !empty($_POST['twofactor']) && is_array($_POST['twofactor']) ? $_POST['twofactor'] : [];
 $notices = [];
 
-$remainingAttempts = $loginAttempts->countRemainingAttempts($ipAddress);
+$remainingAttempts = $msz->authCtx->loginAttempts->countRemainingAttempts($ipAddress);
 
 $tokenString = !empty($_GET['token']) && is_string($_GET['token']) ? $_GET['token'] : (
     !empty($twofactor['token']) && is_string($twofactor['token']) ? $twofactor['token'] : ''
 );
 
-$tokenUserId = $tfaSessions->getTokenUserId($tokenString);
+$tokenUserId = $msz->authCtx->tfaSessions->getTokenUserId($tokenString);
 if(empty($tokenUserId)) {
-    Tools::redirect($urls->format('auth-login'));
+    Tools::redirect($msz->urls->format('auth-login'));
     return;
 }
 
-$userInfo = $users->getUser($tokenUserId, 'id');
+$userInfo = $msz->usersCtx->users->getUser($tokenUserId, 'id');
 
 // checking user_totp_key specifically because there's a fringe chance that
 //  there's a token present, but totp is actually disabled
-if(!$userInfo->hasTOTPKey()) {
-    Tools::redirect($urls->format('auth-login'));
+if(!$userInfo->hasTOTP) {
+    Tools::redirect($msz->urls->format('auth-login'));
     return;
 }
 
@@ -65,7 +57,7 @@ while(!empty($twofactor)) {
     }
 
     $clientInfo = ClientInfo::fromRequest();
-    $totp = new TOTPGenerator($userInfo->getTOTPKey());
+    $totp = new TOTPGenerator($userInfo->totpKey);
 
     if(!in_array($twofactor['code'], $totp->generateRange())) {
         $notices[] = sprintf(
@@ -73,21 +65,21 @@ while(!empty($twofactor)) {
             $remainingAttempts - 1,
             $remainingAttempts === 2 ? '' : 's'
         );
-        $loginAttempts->recordAttempt(false, $ipAddress, $countryCode, $userAgent, $clientInfo, $userInfo);
+        $msz->authCtx->loginAttempts->recordAttempt(false, $ipAddress, $countryCode, $userAgent, $clientInfo, $userInfo);
         break;
     }
 
-    $loginAttempts->recordAttempt(true, $ipAddress, $countryCode, $userAgent, $clientInfo, $userInfo);
-    $tfaSessions->deleteToken($tokenString);
+    $msz->authCtx->loginAttempts->recordAttempt(true, $ipAddress, $countryCode, $userAgent, $clientInfo, $userInfo);
+    $msz->authCtx->tfaSessions->deleteToken($tokenString);
 
     try {
-        $sessionInfo = $sessions->createSession($userInfo, $ipAddress, $countryCode, $userAgent, $clientInfo);
+        $sessionInfo = $msz->authCtx->sessions->createSession($userInfo, $ipAddress, $countryCode, $userAgent, $clientInfo);
     } catch(RuntimeException $ex) {
         $notices[] = "Something broke while creating a session for you, please tell an administrator or developer about this!";
         break;
     }
 
-    $tokenBuilder = $authInfo->getTokenInfo()->toBuilder();
+    $tokenBuilder = $msz->authInfo->tokenInfo->toBuilder();
     $tokenBuilder->setUserId($userInfo);
     $tokenBuilder->setSessionToken($sessionInfo);
     $tokenBuilder->removeImpersonatedUserId();
@@ -96,7 +88,7 @@ while(!empty($twofactor)) {
     AuthTokenCookie::apply($tokenPacker->pack($tokenInfo));
 
     if(!Tools::isLocalURL($redirect))
-        $redirect = $urls->format('index');
+        $redirect = $msz->urls->format('index');
 
     Tools::redirect($redirect);
     return;
@@ -104,7 +96,7 @@ while(!empty($twofactor)) {
 
 Template::render('auth.twofactor', [
     'twofactor_notices' => $notices,
-    'twofactor_redirect' => !empty($_GET['redirect']) && is_string($_GET['redirect']) ? $_GET['redirect'] : $urls->format('index'),
+    'twofactor_redirect' => !empty($_GET['redirect']) && is_string($_GET['redirect']) ? $_GET['redirect'] : $msz->urls->format('index'),
     'twofactor_attempts_remaining' => $remainingAttempts,
     'twofactor_token' => $tokenString,
 ]);
diff --git a/public-legacy/comments.php b/public-legacy/comments.php
index d254a617..11eafea7 100644
--- a/public-legacy/comments.php
+++ b/public-legacy/comments.php
@@ -3,8 +3,7 @@ namespace Misuzu;
 
 use RuntimeException;
 
-$usersCtx = $msz->getUsersContext();
-$redirect = filter_input(INPUT_GET, 'return') ?? $_SERVER['HTTP_REFERER'] ?? $msz->getUrls()->format('index');
+$redirect = filter_input(INPUT_GET, 'return') ?? $_SERVER['HTTP_REFERER'] ?? $msz->urls->format('index');
 
 if(!Tools::isLocalURL($redirect))
     Template::displayInfo('Possible request forgery detected.', 403);
@@ -12,17 +11,13 @@ if(!Tools::isLocalURL($redirect))
 if(!CSRF::validateRequest())
     Template::displayInfo("Couldn't verify this request, please refresh the page and try again.", 403);
 
-$authInfo = $msz->getAuthInfo();
-if(!$authInfo->isLoggedIn())
+if(!$msz->authInfo->isLoggedIn)
     Template::displayInfo('You must be logged in to manage comments.', 403);
 
-$currentUserInfo = $authInfo->getUserInfo();
-
-if($usersCtx->hasActiveBan($currentUserInfo))
+if($msz->usersCtx->hasActiveBan($msz->authInfo->userInfo))
     Template::displayInfo('You have been banned, check your profile for more information.', 403);
 
-$comments = $msz->getComments();
-$perms = $authInfo->getPerms('global');
+$perms = $msz->authInfo->getPerms('global');
 
 $commentId = (string)filter_input(INPUT_GET, 'c', FILTER_SANITIZE_NUMBER_INT);
 $commentMode = (string)filter_input(INPUT_GET, 'm');
@@ -30,12 +25,12 @@ $commentVote = (int)filter_input(INPUT_GET, 'v', FILTER_SANITIZE_NUMBER_INT);
 
 if(!empty($commentId)) {
     try {
-        $commentInfo = $comments->getPost($commentId);
+        $commentInfo = $msz->comments->getPost($commentId);
     } catch(RuntimeException $ex) {
         Template::displayInfo('Post not found.', 404);
     }
 
-    $categoryInfo = $comments->getCategory(postInfo: $commentInfo);
+    $categoryInfo = $msz->comments->getCategory(postInfo: $commentInfo);
 }
 
 if($commentMode !== 'create' && empty($commentInfo))
@@ -44,77 +39,77 @@ if($commentMode !== 'create' && empty($commentInfo))
 switch($commentMode) {
     case 'pin':
     case 'unpin':
-        if(!$perms->check(Perm::G_COMMENTS_PIN) && !$categoryInfo->isOwner($currentUserInfo))
+        if(!$perms->check(Perm::G_COMMENTS_PIN) && !$categoryInfo->isOwner($msz->authInfo->userInfo))
             Template::displayInfo("You're not allowed to pin comments.", 403);
 
-        if($commentInfo->isDeleted())
+        if($commentInfo->deleted)
             Template::displayInfo("This comment doesn't exist!", 400);
 
-        if($commentInfo->isReply())
+        if($commentInfo->isReply)
             Template::displayInfo("You can't pin replies!", 400);
 
         $isPinning = $commentMode === 'pin';
 
         if($isPinning) {
-            if($commentInfo->isPinned())
+            if($commentInfo->pinned)
                 Template::displayInfo('This comment is already pinned.', 400);
 
-            $comments->pinPost($commentInfo);
+            $msz->comments->pinPost($commentInfo);
         } else {
-            if(!$commentInfo->isPinned())
+            if(!$commentInfo->pinned)
                 Template::displayInfo("This comment isn't pinned yet.", 400);
 
-            $comments->unpinPost($commentInfo);
+            $msz->comments->unpinPost($commentInfo);
         }
 
-        Tools::redirect($redirect . '#comment-' . $commentInfo->getId());
+        Tools::redirect($redirect . '#comment-' . $commentInfo->id);
         break;
 
     case 'vote':
-        if(!$perms->check(Perm::G_COMMENTS_VOTE) && !$categoryInfo->isOwner($currentUserInfo))
+        if(!$perms->check(Perm::G_COMMENTS_VOTE) && !$categoryInfo->isOwner($msz->authInfo->userInfo))
             Template::displayInfo("You're not allowed to vote on comments.", 403);
 
-        if($commentInfo->isDeleted())
+        if($commentInfo->deleted)
             Template::displayInfo("This comment doesn't exist!", 400);
 
         if($commentVote > 0)
-            $comments->addPostPositiveVote($commentInfo, $currentUserInfo);
+            $msz->comments->addPostPositiveVote($commentInfo, $msz->authInfo->userInfo);
         elseif($commentVote < 0)
-            $comments->addPostNegativeVote($commentInfo, $currentUserInfo);
+            $msz->comments->addPostNegativeVote($commentInfo, $msz->authInfo->userInfo);
         else
-            $comments->removePostVote($commentInfo, $currentUserInfo);
+            $msz->comments->removePostVote($commentInfo, $msz->authInfo->userInfo);
 
-        Tools::redirect($redirect . '#comment-' . $commentInfo->getId());
+        Tools::redirect($redirect . '#comment-' . $commentInfo->id);
         break;
 
     case 'delete':
         $canDelete = $perms->check(Perm::G_COMMENTS_DELETE_OWN | Perm::G_COMMENTS_DELETE_ANY);
-        if(!$canDelete && !$categoryInfo->isOwner($currentUserInfo))
+        if(!$canDelete && !$categoryInfo->isOwner($msz->authInfo->userInfo))
             Template::displayInfo("You're not allowed to delete comments.", 403);
 
         $canDeleteAny = $perms->check(Perm::G_COMMENTS_DELETE_ANY);
-        if($commentInfo->isDeleted())
+        if($commentInfo->deleted)
             Template::displayInfo(
                 $canDeleteAny ? 'This comment is already marked for deletion.' : "This comment doesn't exist.",
                 400
             );
 
-        $isOwnComment = $commentInfo->getUserId() === $currentUserInfo->getId();
+        $isOwnComment = $commentInfo->userId === $msz->authInfo->userInfo->getId();
         $isModAction  = $canDeleteAny && !$isOwnComment;
 
         if(!$isModAction && !$isOwnComment)
             Template::displayInfo("You're not allowed to delete comments made by others.", 403);
 
-        $comments->deletePost($commentInfo);
+        $msz->comments->deletePost($commentInfo);
 
         if($isModAction) {
             $msz->createAuditLog('COMMENT_ENTRY_DELETE_MOD', [
-                $commentInfo->getId(),
-                $commentUserId = $commentInfo->getUserId(),
+                $commentInfo->id,
+                $commentUserId = $commentInfo->userId,
                 '<username>',
             ]);
         } else {
-            $msz->createAuditLog('COMMENT_ENTRY_DELETE', [$commentInfo->getId()]);
+            $msz->createAuditLog('COMMENT_ENTRY_DELETE', [$commentInfo->id]);
         }
 
         Tools::redirect($redirect);
@@ -124,22 +119,22 @@ switch($commentMode) {
         if(!$perms->check(Perm::G_COMMENTS_DELETE_ANY))
             Template::displayInfo("You're not allowed to restore deleted comments.", 403);
 
-        if(!$commentInfo->isDeleted())
+        if(!$commentInfo->deleted)
             Template::displayInfo("This comment isn't in a deleted state.", 400);
 
-        $comments->restorePost($commentInfo);
+        $msz->comments->restorePost($commentInfo);
 
         $msz->createAuditLog('COMMENT_ENTRY_RESTORE', [
-            $commentInfo->getId(),
-            $commentUserId = $commentInfo->getUserId(),
+            $commentInfo->id,
+            $commentUserId = $commentInfo->userId,
             '<username>',
         ]);
 
-        Tools::redirect($redirect . '#comment-' . $commentInfo->getId());
+        Tools::redirect($redirect . '#comment-' . $commentInfo->id);
         break;
 
     case 'create':
-        if(!$perms->check(Perm::G_COMMENTS_CREATE) && !$categoryInfo->isOwner($currentUserInfo))
+        if(!$perms->check(Perm::G_COMMENTS_CREATE) && !$categoryInfo->isOwner($msz->authInfo->userInfo))
             Template::displayInfo("You're not allowed to post comments.", 403);
 
         if(empty($_POST['comment']) || !is_array($_POST['comment']))
@@ -149,13 +144,13 @@ switch($commentMode) {
             $categoryId = isset($_POST['comment']['category']) && is_string($_POST['comment']['category'])
                 ? (int)$_POST['comment']['category']
                 : 0;
-            $categoryInfo = $comments->getCategory(categoryId: $categoryId);
+            $categoryInfo = $msz->comments->getCategory(categoryId: $categoryId);
         } catch(RuntimeException $ex) {
             Template::displayInfo('This comment category doesn\'t exist.', 404);
         }
 
         $canLock = $perms->check(Perm::G_COMMENTS_LOCK);
-        if($categoryInfo->isLocked() && !$canLock)
+        if($categoryInfo->locked && !$canLock)
             Template::displayInfo('This comment category has been locked.', 403);
 
         $commentText = !empty($_POST['comment']['text'])  && is_string($_POST['comment']['text']) ? $_POST['comment']['text'] : '';
@@ -164,10 +159,10 @@ switch($commentMode) {
         $commentPin = !empty($_POST['comment']['pin']) && $perms->check(Perm::G_COMMENTS_PIN);
 
         if($commentLock) {
-            if($categoryInfo->isLocked())
-                $comments->unlockCategory($categoryInfo);
+            if($categoryInfo->locked)
+                $msz->comments->unlockCategory($categoryInfo);
             else
-                $comments->lockCategory($categoryInfo);
+                $msz->comments->lockCategory($categoryInfo);
         }
 
         if(strlen($commentText) > 0) {
@@ -186,22 +181,22 @@ switch($commentMode) {
 
         if($commentReply > 0) {
             try {
-                $parentInfo = $comments->getPost($commentReply);
+                $parentInfo = $msz->comments->getPost($commentReply);
             } catch(RuntimeException $ex) {}
 
-            if(!isset($parentInfo) || $parentInfo->isDeleted())
+            if(!isset($parentInfo) || $parentInfo->deleted)
                 Template::displayInfo('The comment you tried to reply to does not exist.', 404);
         }
 
-        $commentInfo = $comments->createPost(
+        $commentInfo = $msz->comments->createPost(
             $categoryInfo,
             $parentInfo ?? null,
-            $currentUserInfo,
+            $msz->authInfo->userInfo,
             $commentText,
             $commentPin
         );
 
-        Tools::redirect($redirect . '#comment-' . $commentInfo->getId());
+        Tools::redirect($redirect . '#comment-' . $commentInfo->id);
         break;
 
     default:
diff --git a/public-legacy/forum/forum.php b/public-legacy/forum/forum.php
index 9c2ae79c..845d3649 100644
--- a/public-legacy/forum/forum.php
+++ b/public-legacy/forum/forum.php
@@ -4,43 +4,32 @@ namespace Misuzu;
 use stdClass;
 use RuntimeException;
 
-$forumCtx = $msz->getForumContext();
-$forumCategories = $forumCtx->getCategories();
-$forumTopics = $forumCtx->getTopics();
-$forumPosts = $forumCtx->getPosts();
-$usersCtx = $msz->getUsersContext();
-
 $categoryId = (int)filter_input(INPUT_GET, 'f', FILTER_SANITIZE_NUMBER_INT);
 
 try {
-    $categoryInfo = $forumCategories->getCategory(categoryId: $categoryId);
+    $categoryInfo = $msz->forumCtx->categories->getCategory(categoryId: $categoryId);
 } catch(RuntimeException $ex) {
     Template::throwError(404);
 }
 
-$authInfo = $msz->getAuthInfo();
-$perms = $authInfo->getPerms('forum', $categoryInfo);
+$perms = $msz->authInfo->getPerms('forum', $categoryInfo);
 
-$currentUser = $authInfo->getUserInfo();
+$currentUser = $msz->authInfo->userInfo;
 $currentUserId = $currentUser === null ? '0' : $currentUser->getId();
 
 if(!$perms->check(Perm::F_CATEGORY_VIEW))
     Template::throwError(403);
 
-if($usersCtx->hasActiveBan($currentUser))
+if($msz->usersCtx->hasActiveBan($currentUser))
     $perms = $perms->apply(fn($calc) => $calc & (Perm::F_CATEGORY_LIST | Perm::F_CATEGORY_VIEW));
 
-if($categoryInfo->isLink()) {
-    if($categoryInfo->hasLinkTarget()) {
-        $forumCategories->incrementCategoryClicks($categoryInfo);
-        Tools::redirect($categoryInfo->getLinkTarget());
-        return;
-    }
-
-    Template::throwError(404);
+if($categoryInfo->isLink) {
+    $msz->forumCtx->categories->incrementCategoryClicks($categoryInfo);
+    Tools::redirect($categoryInfo->linkTarget ?? '/');
+    return;
 }
 
-$forumPagination = new Pagination($forumTopics->countTopics(
+$forumPagination = new Pagination($msz->forumCtx->topics->countTopics(
     categoryInfo: $categoryInfo,
     global: true,
     deleted: $perms->check(Perm::F_POST_DELETE_ANY) ? null : false
@@ -52,11 +41,11 @@ if(!$forumPagination->hasValidOffset())
 $children = [];
 $topics = [];
 
-if($categoryInfo->mayHaveChildren()) {
-    $children = $forumCategories->getCategoryChildren($categoryInfo, hidden: false, asTree: true);
+if($categoryInfo->mayHaveChildren) {
+    $children = $msz->forumCtx->categories->getCategoryChildren($categoryInfo, hidden: false, asTree: true);
 
     foreach($children as $childId => $child) {
-        $childPerms = $authInfo->getPerms('forum', $child->info);
+        $childPerms = $msz->authInfo->getPerms('forum', $child->info);
         if(!$childPerms->check(Perm::F_CATEGORY_LIST)) {
             unset($category->children[$childId]);
             continue;
@@ -64,9 +53,9 @@ if($categoryInfo->mayHaveChildren()) {
 
         $childUnread = false;
 
-        if($child->info->mayHaveChildren()) {
+        if($child->info->mayHaveChildren) {
             foreach($child->children as $grandChildId => $grandChild) {
-                $grandChildPerms = $authInfo->getPerms('forum', $grandChild->info);
+                $grandChildPerms = $msz->authInfo->getPerms('forum', $grandChild->info);
                 if(!$grandChildPerms->check(Perm::F_CATEGORY_LIST)) {
                     unset($child->children[$grandChildId]);
                     continue;
@@ -74,15 +63,15 @@ if($categoryInfo->mayHaveChildren()) {
 
                 $grandChildUnread = false;
 
-                if($grandChild->info->mayHaveTopics()) {
-                    $catIds = [$grandChild->info->getId()];
+                if($grandChild->info->mayHaveTopics) {
+                    $catIds = [$grandChild->info->id];
                     foreach($grandChild->childIds as $greatGrandChildId) {
-                        $greatGrandChildPerms = $authInfo->getPerms('forum', $greatGrandChildId);
+                        $greatGrandChildPerms = $msz->authInfo->getPerms('forum', $greatGrandChildId);
                         if(!$greatGrandChildPerms->check(Perm::F_CATEGORY_LIST))
                             $catIds[] = $greatGrandChildId;
                     }
 
-                    $grandChildUnread = $forumCategories->checkCategoryUnread($catIds, $currentUser);
+                    $grandChildUnread = $msz->forumCtx->categories->checkCategoryUnread($catIds, $currentUser);
                     if($grandChildUnread)
                         $childUnread = true;
                 }
@@ -92,16 +81,16 @@ if($categoryInfo->mayHaveChildren()) {
             }
         }
 
-        if($child->info->mayHaveChildren() || $child->info->mayHaveTopics()) {
-            $catIds = [$child->info->getId()];
+        if($child->info->mayHaveChildren || $child->info->mayHaveTopics) {
+            $catIds = [$child->info->id];
             foreach($child->childIds as $grandChildId) {
-                $grandChildPerms = $authInfo->getPerms('forum', $grandChildId);
+                $grandChildPerms = $msz->authInfo->getPerms('forum', $grandChildId);
                 if($grandChildPerms->check(Perm::F_CATEGORY_LIST))
                     $catIds[] = $grandChildId;
             }
 
             try {
-                $lastPostInfo = $forumPosts->getPost(categoryInfos: $catIds, getLast: true, deleted: false);
+                $lastPostInfo = $msz->forumCtx->posts->getPost(categoryInfos: $catIds, getLast: true, deleted: false);
             } catch(RuntimeException $ex) {
                 $lastPostInfo = null;
             }
@@ -109,25 +98,25 @@ if($categoryInfo->mayHaveChildren()) {
             if($lastPostInfo !== null) {
                 $child->lastPost = new stdClass;
                 $child->lastPost->info = $lastPostInfo;
-                $child->lastPost->topicInfo = $forumTopics->getTopic(postInfo: $lastPostInfo);
+                $child->lastPost->topicInfo = $msz->forumCtx->topics->getTopic(postInfo: $lastPostInfo);
 
-                if($lastPostInfo->hasUserId()) {
-                    $child->lastPost->user = $usersCtx->getUserInfo($lastPostInfo->getUserId());
-                    $child->lastPost->colour = $usersCtx->getUserColour($child->lastPost->user);
+                if($lastPostInfo->userId !== null) {
+                    $child->lastPost->user = $msz->usersCtx->getUserInfo($lastPostInfo->userId);
+                    $child->lastPost->colour = $msz->usersCtx->getUserColour($child->lastPost->user);
                 }
             }
         }
 
-        if($child->info->mayHaveTopics() && !$childUnread)
-            $childUnread = $forumCategories->checkCategoryUnread($child->info, $currentUser);
+        if($child->info->mayHaveTopics && !$childUnread)
+            $childUnread = $msz->forumCtx->categories->checkCategoryUnread($child->info, $currentUser);
 
         $child->perms = $childPerms;
         $child->unread = $childUnread;
     }
 }
 
-if($categoryInfo->mayHaveTopics()) {
-    $topicInfos = $forumTopics->getTopics(
+if($categoryInfo->mayHaveTopics) {
+    $topicInfos = $msz->forumCtx->topics->getTopics(
         categoryInfo: $categoryInfo,
         global: true,
         deleted: $perms->check(Perm::F_POST_DELETE_ANY) ? null : false,
@@ -137,25 +126,25 @@ if($categoryInfo->mayHaveTopics()) {
     foreach($topicInfos as $topicInfo) {
         $topics[] = $topic = new stdClass;
         $topic->info = $topicInfo;
-        $topic->unread = $forumTopics->checkTopicUnread($topicInfo, $currentUser);
-        $topic->participated = $forumTopics->checkTopicParticipated($topicInfo, $currentUser);
+        $topic->unread = $msz->forumCtx->topics->checkTopicUnread($topicInfo, $currentUser);
+        $topic->participated = $msz->forumCtx->topics->checkTopicParticipated($topicInfo, $currentUser);
 
-        if($topicInfo->hasUserId()) {
-            $topic->user = $usersCtx->getUserInfo($topicInfo->getUserId());
-            $topic->colour = $usersCtx->getUserColour($topic->user);
+        if($topicInfo->userId !== null) {
+            $topic->user = $msz->usersCtx->getUserInfo($topicInfo->userId);
+            $topic->colour = $msz->usersCtx->getUserColour($topic->user);
         }
 
         try {
             $topic->lastPost = new stdClass;
-            $topic->lastPost->info = $lastPostInfo = $forumPosts->getPost(
+            $topic->lastPost->info = $lastPostInfo = $msz->forumCtx->posts->getPost(
                 topicInfo: $topicInfo,
                 getLast: true,
-                deleted: $topicInfo->isDeleted() ? null : false,
+                deleted: $topicInfo->deleted ? null : false,
             );
 
-            if($lastPostInfo->hasUserId()) {
-                $topic->lastPost->user = $usersCtx->getUserInfo($lastPostInfo->getUserId());
-                $topic->lastPost->colour = $usersCtx->getUserColour($topic->lastPost->user);
+            if($lastPostInfo->userId !== null) {
+                $topic->lastPost->user = $msz->usersCtx->getUserInfo($lastPostInfo->userId);
+                $topic->lastPost->colour = $msz->usersCtx->getUserColour($topic->lastPost->user);
             }
         } catch(RuntimeException $ex) {
             $topic->lastPost = null;
@@ -168,8 +157,8 @@ $perms = $perms->checkMany([
 ]);
 
 Template::render('forum.forum', [
-    'forum_breadcrumbs' => iterator_to_array($forumCategories->getCategoryAncestry($categoryInfo)),
-    'global_accent_colour' => $forumCategories->getCategoryColour($categoryInfo),
+    'forum_breadcrumbs' => iterator_to_array($msz->forumCtx->categories->getCategoryAncestry($categoryInfo)),
+    'global_accent_colour' => $msz->forumCtx->categories->getCategoryColour($categoryInfo),
     'forum_info' => $categoryInfo,
     'forum_children' => $children,
     'forum_topics' => $topics,
diff --git a/public-legacy/forum/index.php b/public-legacy/forum/index.php
index fec71b69..ef29a3a0 100644
--- a/public-legacy/forum/index.php
+++ b/public-legacy/forum/index.php
@@ -4,42 +4,36 @@ namespace Misuzu;
 use stdClass;
 use RuntimeException;
 
-$forumCtx = $msz->getForumContext();
-$forumCategories = $forumCtx->getCategories();
-$forumTopics = $forumCtx->getTopics();
-$forumPosts = $forumCtx->getPosts();
-$usersCtx = $msz->getUsersContext();
 $mode = (string)filter_input(INPUT_GET, 'm');
 
-$authInfo = $msz->getAuthInfo();
-$currentUser = $authInfo->getUserInfo();
+$currentUser = $msz->authInfo->userInfo;
 $currentUserId = $currentUser === null ? '0' : $currentUser->getId();
 
 if($mode === 'mark') {
-    if(!$authInfo->isLoggedIn())
+    if(!$msz->authInfo->isLoggedIn)
         Template::throwError(403);
 
     $categoryId = filter_input(INPUT_GET, 'f', FILTER_SANITIZE_NUMBER_INT);
 
     if($_SERVER['REQUEST_METHOD'] === 'POST' && CSRF::validateRequest()) {
         $categoryInfos = $categoryId === null
-            ? $forumCategories->getCategories()
-            : $forumCategories->getCategoryChildren(parentInfo: $categoryId, includeSelf: true);
+            ? $msz->forumCtx->categories->getCategories()
+            : $msz->forumCtx->categories->getCategoryChildren(parentInfo: $categoryId, includeSelf: true);
 
         foreach($categoryInfos as $categoryInfo) {
-            $perms = $authInfo->getPerms('forum', $categoryInfo);
+            $perms = $msz->authInfo->getPerms('forum', $categoryInfo);
             if($perms->check(Perm::F_CATEGORY_LIST))
-                $forumCategories->updateUserReadCategory($userInfo, $categoryInfo);
+                $msz->forumCtx->categories->updateUserReadCategory($userInfo, $categoryInfo);
         }
 
-        Tools::redirect($msz->getUrls()->format($categoryId ? 'forum-category' : 'forum-index', ['forum' => $categoryId]));
+        Tools::redirect($msz->urls->format($categoryId ? 'forum-category' : 'forum-index', ['forum' => $categoryId]));
         return;
     }
 
     Template::render('confirm', [
         'title' => 'Mark forum as read',
         'message' => 'Are you sure you want to mark ' . ($categoryId < 1 ? 'the entire' : 'this') . ' forum as read?',
-        'return' => $msz->getUrls()->format($categoryId ? 'forum-category' : 'forum-index', ['forum' => $categoryId]),
+        'return' => $msz->urls->format($categoryId ? 'forum-category' : 'forum-index', ['forum' => $categoryId]),
         'params' => [
             'forum' => $categoryId,
         ]
@@ -50,10 +44,10 @@ if($mode === 'mark') {
 if($mode !== '')
     Template::throwError(404);
 
-$categories = $forumCategories->getCategories(hidden: false, asTree: true);
+$categories = $msz->forumCtx->categories->getCategories(hidden: false, asTree: true);
 
 foreach($categories as $categoryId => $category) {
-    $perms = $authInfo->getPerms('forum', $category->info);
+    $perms = $msz->authInfo->getPerms('forum', $category->info);
     if(!$perms->check(Perm::F_CATEGORY_LIST)) {
         unset($categories[$categoryId]);
         continue;
@@ -61,9 +55,9 @@ foreach($categories as $categoryId => $category) {
 
     $unread = false;
 
-    if($category->info->mayHaveChildren())
+    if($category->info->mayHaveChildren)
         foreach($category->children as $childId => $child) {
-            $childPerms = $authInfo->getPerms('forum', $child->info);
+            $childPerms = $msz->authInfo->getPerms('forum', $child->info);
             if(!$childPerms->check(Perm::F_CATEGORY_LIST)) {
                 unset($category->children[$childId]);
                 continue;
@@ -71,10 +65,10 @@ foreach($categories as $categoryId => $category) {
 
             $childUnread = false;
 
-            if($category->info->isListing()) {
-                if($child->info->mayHaveChildren()) {
+            if($category->info->isListing) {
+                if($child->info->mayHaveChildren) {
                     foreach($child->children as $grandChildId => $grandChild) {
-                        $grandChildPerms = $authInfo->getPerms('forum', $grandChild->info);
+                        $grandChildPerms = $msz->authInfo->getPerms('forum', $grandChild->info);
                         if(!$grandChildPerms->check(Perm::F_CATEGORY_LIST)) {
                             unset($child->children[$grandChildId]);
                             continue;
@@ -82,15 +76,15 @@ foreach($categories as $categoryId => $category) {
 
                         $grandChildUnread = false;
 
-                        if($grandChild->info->mayHaveTopics()) {
-                            $catIds = [$grandChild->info->getId()];
+                        if($grandChild->info->mayHaveTopics) {
+                            $catIds = [$grandChild->info->id];
                             foreach($grandChild->childIds as $greatGrandChildId) {
-                                $greatGrandChildPerms = $authInfo->getPerms('forum', $greatGrandChildId);
+                                $greatGrandChildPerms = $msz->authInfo->getPerms('forum', $greatGrandChildId);
                                 if($greatGrandChildPerms->check(Perm::F_CATEGORY_LIST))
                                     $catIds[] = $greatGrandChildId;
                             }
 
-                            $grandChildUnread = $forumCategories->checkCategoryUnread($catIds, $currentUser);
+                            $grandChildUnread = $msz->forumCtx->categories->checkCategoryUnread($catIds, $currentUser);
                             if($grandChildUnread)
                                 $childUnread = true;
                         }
@@ -100,16 +94,16 @@ foreach($categories as $categoryId => $category) {
                     }
                 }
 
-                if($child->info->mayHaveChildren() || $child->info->mayHaveTopics()) {
-                    $catIds = [$child->info->getId()];
+                if($child->info->mayHaveChildren || $child->info->mayHaveTopics) {
+                    $catIds = [$child->info->id];
                     foreach($child->childIds as $grandChildId) {
-                        $grandChildPerms = $authInfo->getPerms('forum', $grandChildId);
+                        $grandChildPerms = $msz->authInfo->getPerms('forum', $grandChildId);
                         if($grandChildPerms->check(Perm::F_CATEGORY_LIST))
                             $catIds[] = $grandChildId;
                     }
 
                     try {
-                        $lastPostInfo = $forumPosts->getPost(categoryInfos: $catIds, getLast: true, deleted: false);
+                        $lastPostInfo = $msz->forumCtx->posts->getPost(categoryInfos: $catIds, getLast: true, deleted: false);
                     } catch(RuntimeException $ex) {
                         $lastPostInfo = null;
                     }
@@ -117,18 +111,18 @@ foreach($categories as $categoryId => $category) {
                     if($lastPostInfo !== null) {
                         $child->lastPost = new stdClass;
                         $child->lastPost->info = $lastPostInfo;
-                        $child->lastPost->topicInfo = $forumTopics->getTopic(postInfo: $lastPostInfo);
+                        $child->lastPost->topicInfo = $msz->forumCtx->topics->getTopic(postInfo: $lastPostInfo);
 
-                        if($lastPostInfo->hasUserId()) {
-                            $child->lastPost->user = $usersCtx->getUserInfo($lastPostInfo->getUserId());
-                            $child->lastPost->colour = $usersCtx->getUserColour($child->lastPost->user);
+                        if($lastPostInfo->userId !== null) {
+                            $child->lastPost->user = $msz->usersCtx->getUserInfo($lastPostInfo->userId);
+                            $child->lastPost->colour = $msz->usersCtx->getUserColour($child->lastPost->user);
                         }
                     }
                 }
             }
 
-            if($child->info->mayHaveTopics() && !$childUnread) {
-                $childUnread = $forumCategories->checkCategoryUnread($child->info, $currentUser);
+            if($child->info->mayHaveTopics && !$childUnread) {
+                $childUnread = $msz->forumCtx->categories->checkCategoryUnread($child->info, $currentUser);
                 if($childUnread)
                     $unread = true;
             }
@@ -137,10 +131,10 @@ foreach($categories as $categoryId => $category) {
             $child->unread = $childUnread;
         }
 
-    if($category->info->mayHaveTopics() && !$unread)
-        $unread = $forumCategories->checkCategoryUnread($category->info, $currentUser);
+    if($category->info->mayHaveTopics && !$unread)
+        $unread = $msz->forumCtx->categories->checkCategoryUnread($category->info, $currentUser);
 
-    if(!$category->info->isListing()) {
+    if(!$category->info->isListing) {
         if(!array_key_exists('0', $categories)) {
             $categories['0'] = $root = new stdClass;
             $root->info = null;
@@ -153,16 +147,16 @@ foreach($categories as $categoryId => $category) {
         $categories['0']->children[$categoryId] = $category;
         unset($categories[$categoryId]);
 
-        if($category->info->mayHaveChildren() || $category->info->mayHaveTopics()) {
-            $catIds = [$category->info->getId()];
+        if($category->info->mayHaveChildren || $category->info->mayHaveTopics) {
+            $catIds = [$category->info->id];
             foreach($category->childIds as $childId) {
-                $childPerms = $authInfo->getPerms('forum', $childId);
+                $childPerms = $msz->authInfo->getPerms('forum', $childId);
                 if($childPerms->check(Perm::F_CATEGORY_LIST))
                     $catIds[] = $childId;
             }
 
             try {
-                $lastPostInfo = $forumPosts->getPost(categoryInfos: $catIds, getLast: true, deleted: false);
+                $lastPostInfo = $msz->forumCtx->posts->getPost(categoryInfos: $catIds, getLast: true, deleted: false);
             } catch(RuntimeException $ex) {
                 $lastPostInfo = null;
             }
@@ -170,11 +164,11 @@ foreach($categories as $categoryId => $category) {
             if($lastPostInfo !== null) {
                 $category->lastPost = new stdClass;
                 $category->lastPost->info = $lastPostInfo;
-                $category->lastPost->topicInfo = $forumTopics->getTopic(postInfo: $lastPostInfo);
+                $category->lastPost->topicInfo = $msz->forumCtx->topics->getTopic(postInfo: $lastPostInfo);
 
-                if($lastPostInfo->hasUserId()) {
-                    $category->lastPost->user = $usersCtx->getUserInfo($lastPostInfo->getUserId());
-                    $category->lastPost->colour = $usersCtx->getUserColour($category->lastPost->user);
+                if($lastPostInfo->userId !== null) {
+                    $category->lastPost->user = $msz->usersCtx->getUserInfo($lastPostInfo->userId);
+                    $category->lastPost->colour = $msz->usersCtx->getUserColour($category->lastPost->user);
                 }
             }
         }
diff --git a/public-legacy/forum/leaderboard.php b/public-legacy/forum/leaderboard.php
index 19ee0616..e8876f5c 100644
--- a/public-legacy/forum/leaderboard.php
+++ b/public-legacy/forum/leaderboard.php
@@ -3,11 +3,9 @@ namespace Misuzu;
 
 use RuntimeException;
 
-if(!$msz->getAuthInfo()->getPerms('global')->check(Perm::G_FORUM_LEADERBOARD_VIEW))
+if(!$msz->authInfo->getPerms('global')->check(Perm::G_FORUM_LEADERBOARD_VIEW))
     Template::throwError(403);
 
-$forumCtx = $msz->getForumContext();
-$usersCtx = $msz->getUsersContext();
 $config = $cfg->getValues([
     ['forum_leader.first_year:i', 2018],
     ['forum_leader.first_month:i', 12],
@@ -61,12 +59,12 @@ for($i = $currentYear, $j = $currentMonth;;) {
         break;
 }
 
-$rankings = $forumCtx->getPosts()->generatePostRankings($year, $month, $unrankedForums, $unrankedTopics);
+$rankings = $msz->forumCtx->posts->generatePostRankings($year, $month, $unrankedForums, $unrankedTopics);
 foreach($rankings as $ranking) {
     $ranking->user = $ranking->colour = null;
 
     if($ranking->userId !== '')
-        $ranking->user = $usersCtx->getUserInfo($ranking->userId);
+        $ranking->user = $msz->usersCtx->getUserInfo($ranking->userId);
 }
 
 $name = 'All Time';
@@ -92,9 +90,9 @@ MD;
     foreach($rankings as $ranking) {
         $totalPostsCount += $ranking->postsCount;
         $markdown .= sprintf("| %s | [%s](%s%s) | %s |\r\n", $ranking->position,
-            $ranking->user?->getName() ?? 'Deleted User',
-            $msz->getSiteInfo()->getURL(),
-            $msz->getUrls()->format('user-profile', ['user' => $ranking->userId]),
+            $ranking->user?->name ?? 'Deleted User',
+            $msz->siteInfo->url,
+            $msz->urls->format('user-profile', ['user' => $ranking->userId]),
             number_format($ranking->postsCount));
     }
 
diff --git a/public-legacy/forum/post.php b/public-legacy/forum/post.php
index 31f45010..5dea152e 100644
--- a/public-legacy/forum/post.php
+++ b/public-legacy/forum/post.php
@@ -3,34 +3,28 @@ namespace Misuzu;
 
 use RuntimeException;
 
-$urls = $msz->getUrls();
-$forumCtx = $msz->getForumContext();
-$forumPosts = $forumCtx->getPosts();
-$usersCtx = $msz->getUsersContext();
-
 $postId = !empty($_GET['p']) && is_string($_GET['p']) ? (int)$_GET['p'] : 0;
 $postMode = !empty($_GET['m']) && is_string($_GET['m']) ? (string)$_GET['m'] : '';
 $submissionConfirmed = !empty($_GET['confirm']) && is_string($_GET['confirm']) && $_GET['confirm'] === '1';
 
 $postRequestVerified = CSRF::validateRequest();
 
-$authInfo = $msz->getAuthInfo();
-if(!empty($postMode) && !$authInfo->isLoggedIn())
+if(!empty($postMode) && !$msz->authInfo->isLoggedIn)
     Template::displayInfo('You must be logged in to manage posts.', 401);
 
-$currentUser = $authInfo->getUserInfo();
+$currentUser = $msz->authInfo->userInfo;
 $currentUserId = $currentUser === null ? '0' : $currentUser->getId();
 
-if($postMode !== '' && $usersCtx->hasActiveBan($currentUser))
+if($postMode !== '' && $msz->usersCtx->hasActiveBan($currentUser))
     Template::displayInfo('You have been banned, check your profile for more information.', 403);
 
 try {
-    $postInfo = $forumPosts->getPost(postId: $postId);
+    $postInfo = $msz->forumCtx->posts->getPost(postId: $postId);
 } catch(RuntimeException $ex) {
     Template::throwError(404);
 }
 
-$perms = $authInfo->getPerms('forum', $postInfo->getCategoryId());
+$perms = $msz->authInfo->getPerms('forum', $postInfo->categoryId);
 
 if(!$perms->check(Perm::F_CATEGORY_VIEW))
     Template::throwError(403);
@@ -40,48 +34,48 @@ $canDeleteAny = $perms->check(Perm::F_POST_DELETE_ANY);
 switch($postMode) {
     case 'delete':
         if($canDeleteAny) {
-            if($postInfo->isDeleted())
+            if($postInfo->deleted)
                 Template::displayInfo('This post has already been marked as deleted.', 404);
         } else {
-            if($postInfo->isDeleted())
+            if($postInfo->deleted)
                 Template::throwError(404);
 
             if(!$perms->check(Perm::F_POST_DELETE_OWN))
                 Template::displayInfo('You are not allowed to delete posts.', 403);
 
-            if($postInfo->getUserId() !== $currentUser->getId())
+            if($postInfo->userId !== $currentUser->getId())
                 Template::displayInfo('You can only delete your own posts.', 403);
 
             // posts may only be deleted within a week of creation, this should be a config value
             $deleteTimeFrame = 60 * 60 * 24 * 7;
-            if($postInfo->getCreatedTime() < time() - $deleteTimeFrame)
+            if($postInfo->createdTime < time() - $deleteTimeFrame)
                 Template::displayInfo('This post has existed for too long. Ask a moderator to remove if it absolutely necessary.', 403);
         }
 
-        $originalPostInfo = $forumPosts->getPost(topicInfo: $postInfo->getTopicId());
-        if($originalPostInfo->getId() === $postInfo->getId())
+        $originalPostInfo = $msz->forumCtx->posts->getPost(topicInfo: $postInfo->topicId);
+        if($originalPostInfo->id === $postInfo->id)
             Template::displayInfo('This is the opening post of the topic it belongs to, it may not be deleted without deleting the entire topic as well.', 403);
 
         if($postRequestVerified && !$submissionConfirmed) {
-            Tools::redirect($urls->format('forum-post', ['post' => $postInfo->getId()]));
+            Tools::redirect($msz->urls->format('forum-post', ['post' => $postInfo->id]));
             break;
         } elseif(!$postRequestVerified) {
             Template::render('forum.confirm', [
                 'title' => 'Confirm post deletion',
                 'class' => 'far fa-trash-alt',
-                'message' => sprintf('You are about to delete post #%d. Are you sure about that?', $postInfo->getId()),
+                'message' => sprintf('You are about to delete post #%d. Are you sure about that?', $postInfo->id),
                 'params' => [
-                    'p' => $postInfo->getId(),
+                    'p' => $postInfo->id,
                     'm' => 'delete',
                 ],
             ]);
             break;
         }
 
-        $forumPosts->deletePost($postInfo);
-        $msz->createAuditLog('FORUM_POST_DELETE', [$postInfo->getId()]);
+        $msz->forumCtx->posts->deletePost($postInfo);
+        $msz->createAuditLog('FORUM_POST_DELETE', [$postInfo->id]);
 
-        Tools::redirect($urls->format('forum-topic', ['topic' => $postInfo->getTopicId()]));
+        Tools::redirect($msz->urls->format('forum-topic', ['topic' => $postInfo->topicId]));
         break;
 
     case 'nuke':
@@ -89,25 +83,25 @@ switch($postMode) {
             Template::throwError(403);
 
         if($postRequestVerified && !$submissionConfirmed) {
-            Tools::redirect($urls->format('forum-post', ['post' => $postInfo->getId()]));
+            Tools::redirect($msz->urls->format('forum-post', ['post' => $postInfo->id]));
             break;
         } elseif(!$postRequestVerified) {
             Template::render('forum.confirm', [
                 'title' => 'Confirm post nuke',
                 'class' => 'fas fa-radiation',
-                'message' => sprintf('You are about to PERMANENTLY DELETE post #%d. Are you sure about that?', $postInfo->getId()),
+                'message' => sprintf('You are about to PERMANENTLY DELETE post #%d. Are you sure about that?', $postInfo->id),
                 'params' => [
-                    'p' => $postInfo->getId(),
+                    'p' => $postInfo->id,
                     'm' => 'nuke',
                 ],
             ]);
             break;
         }
 
-        $forumPosts->nukePost($postInfo->getId());
-        $msz->createAuditLog('FORUM_POST_NUKE', [$postInfo->getId()]);
+        $msz->forumCtx->posts->nukePost($postInfo->id);
+        $msz->createAuditLog('FORUM_POST_NUKE', [$postInfo->id]);
 
-        Tools::redirect($urls->format('forum-topic', ['topic' => $postInfo->getTopicId()]));
+        Tools::redirect($msz->urls->format('forum-topic', ['topic' => $postInfo->topicId]));
         break;
 
     case 'restore':
@@ -115,28 +109,28 @@ switch($postMode) {
             Template::throwError(403);
 
         if($postRequestVerified && !$submissionConfirmed) {
-            Tools::redirect($urls->format('forum-post', ['post' => $postInfo->getId()]));
+            Tools::redirect($msz->urls->format('forum-post', ['post' => $postInfo->id]));
             break;
         } elseif(!$postRequestVerified) {
             Template::render('forum.confirm', [
                 'title' => 'Confirm post restore',
                 'class' => 'fas fa-magic',
-                'message' => sprintf('You are about to restore post #%d. Are you sure about that?', $postInfo->getId()),
+                'message' => sprintf('You are about to restore post #%d. Are you sure about that?', $postInfo->id),
                 'params' => [
-                    'p' => $postInfo->getId(),
+                    'p' => $postInfo->id,
                     'm' => 'restore',
                 ],
             ]);
             break;
         }
 
-        $forumPosts->restorePost($postInfo->getId());
-        $msz->createAuditLog('FORUM_POST_RESTORE', [$postInfo->getId()]);
+        $msz->forumCtx->posts->restorePost($postInfo->id);
+        $msz->createAuditLog('FORUM_POST_RESTORE', [$postInfo->id]);
 
-        Tools::redirect($urls->format('forum-topic', ['topic' => $postInfo->getTopicId()]));
+        Tools::redirect($msz->urls->format('forum-topic', ['topic' => $postInfo->topicId]));
         break;
 
     default: // function as an alt for topic.php?p= by default
-        Tools::redirect($urls->format('forum-post', ['post' => $postInfo->getId()]));
+        Tools::redirect($msz->urls->format('forum-post', ['post' => $postInfo->id]));
         break;
 }
diff --git a/public-legacy/forum/posting.php b/public-legacy/forum/posting.php
index 173ac719..df44aab9 100644
--- a/public-legacy/forum/posting.php
+++ b/public-legacy/forum/posting.php
@@ -8,19 +8,12 @@ use Misuzu\Parsers\Parser;
 use Index\XDateTime;
 use Carbon\CarbonImmutable;
 
-$authInfo = $msz->getAuthInfo();
-if(!$authInfo->isLoggedIn())
+if(!$msz->authInfo->isLoggedIn)
     Template::throwError(401);
 
-$forumCtx = $msz->getForumContext();
-$forumCategories = $forumCtx->getCategories();
-$forumTopics = $forumCtx->getTopics();
-$forumPosts = $forumCtx->getPosts();
-$usersCtx = $msz->getUsersContext();
-
-$currentUser = $authInfo->getUserInfo();
+$currentUser = $msz->authInfo->userInfo;
 $currentUserId = $currentUser->getId();
-if($usersCtx->hasActiveBan($currentUser))
+if($msz->usersCtx->hasActiveBan($currentUser))
     Template::throwError(403);
 
 $forumPostingModes = [
@@ -65,16 +58,16 @@ if(empty($postId)) {
     $hasPostInfo = false;
 } else {
     try {
-        $postInfo = $forumPosts->getPost(postId: $postId);
+        $postInfo = $msz->forumCtx->posts->getPost(postId: $postId);
     } catch(RuntimeException $ex) {
         Template::throwError(404);
     }
 
-    if($postInfo->isDeleted())
+    if($postInfo->deleted)
         Template::throwError(404);
 
     // should automatic cross-quoting be a thing? if so, check if $topicId is < 1 first <-- what did i mean by this?
-    $topicId = $postInfo->getTopicId();
+    $topicId = $postInfo->topicId;
     $hasPostInfo = true;
 }
 
@@ -82,16 +75,16 @@ if(empty($topicId)) {
     $hasTopicInfo = false;
 } else {
     try {
-        $topicInfo = $forumTopics->getTopic(topicId: $topicId);
+        $topicInfo = $msz->forumCtx->topics->getTopic(topicId: $topicId);
     } catch(RuntimeException $ex) {
         Template::throwError(404);
     }
 
-    if($topicInfo->isDeleted())
+    if($topicInfo->deleted)
         Template::throwError(404);
 
-    $forumId = $topicInfo->getCategoryId();
-    $originalPostInfo = $forumPosts->getPost(topicInfo: $topicInfo);
+    $forumId = $topicInfo->categoryId;
+    $originalPostInfo = $msz->forumCtx->posts->getPost(topicInfo: $topicInfo);
     $hasTopicInfo = true;
 }
 
@@ -99,7 +92,7 @@ if(empty($forumId)) {
     $hasCategoryInfo = false;
 } else {
     try {
-        $categoryInfo = $forumCategories->getCategory(categoryId: $forumId);
+        $categoryInfo = $msz->forumCtx->categories->getCategory(categoryId: $forumId);
     } catch(RuntimeException $ex) {
         Template::throwError(404);
     }
@@ -107,16 +100,16 @@ if(empty($forumId)) {
     $hasCategoryInfo = true;
 }
 
-$perms = $authInfo->getPerms('forum', $categoryInfo);
+$perms = $msz->authInfo->getPerms('forum', $categoryInfo);
 
-if($categoryInfo->isArchived()
-    || (isset($topicInfo) && $topicInfo->isLocked() && !$perms->check(Perm::F_TOPIC_LOCK))
+if($categoryInfo->archived
+    || (isset($topicInfo) && $topicInfo->locked && !$perms->check(Perm::F_TOPIC_LOCK))
     || !$perms->check(Perm::F_CATEGORY_VIEW)
     || !$perms->check(Perm::F_POST_CREATE)
     || (!isset($topicInfo) && !$perms->check(Perm::F_TOPIC_CREATE)))
     Template::throwError(403);
 
-if(!$categoryInfo->mayHaveTopics())
+if(!$categoryInfo->mayHaveTopics)
     Template::throwError(400);
 
 $topicTypes = [];
@@ -133,7 +126,7 @@ if($mode === 'create' || $mode === 'edit') {
 }
 
 // edit mode stuff
-if($mode === 'edit' && !$perms->check($postInfo->getUserId() === $currentUserId ? Perm::F_POST_EDIT_OWN : Perm::F_POST_EDIT_ANY))
+if($mode === 'edit' && !$perms->check($postInfo->userId === $currentUserId ? Perm::F_POST_EDIT_OWN : Perm::F_POST_EDIT_ANY))
     Template::throwError(403);
 
 $notices = [];
@@ -148,13 +141,13 @@ if(!empty($_POST)) {
     if(!CSRF::validateRequest()) {
         $notices[] = 'Could not verify request.';
     } else {
-        $isEditingTopic = empty($topicInfo) || ($mode === 'edit' && $originalPostInfo->getId() == $postInfo->getId());
+        $isEditingTopic = empty($topicInfo) || ($mode === 'edit' && $originalPostInfo->id == $postInfo->id);
 
         if($mode === 'create') {
             $postTimeout = $cfg->getInteger('forum.posting.timeout', 5);
             if($postTimeout > 0) {
                 $postTimeoutThreshold = new CarbonImmutable(sprintf('-%d seconds', $postTimeout));
-                $lastPostCreatedAt = $forumPosts->getUserLastPostCreatedAt($currentUser);
+                $lastPostCreatedAt = $msz->forumCtx->posts->getUserLastPostCreatedAt($currentUser);
 
                 if(XDateTime::compare($lastPostCreatedAt, $postTimeoutThreshold) > 0) {
                     $waitSeconds = $postTimeout + ((int)$lastPostCreatedAt->format('U') - time());
@@ -166,9 +159,9 @@ if(!empty($_POST)) {
         }
 
         if($isEditingTopic) {
-            $originalTopicTitle = $topicInfo?->getTitle() ?? null;
+            $originalTopicTitle = $topicInfo?->title ?? null;
             $topicTitleChanged = $topicTitle !== $originalTopicTitle;
-            $originalTopicType = $topicInfo?->getTypeString() ?? 'discussion';
+            $originalTopicType = $topicInfo?->typeString ?? 'discussion';
             $topicTypeChanged = $topicType !== null && $topicType !== $originalTopicType;
 
             $topicTitleLengths = $cfg->getValues([
@@ -207,19 +200,19 @@ if(!empty($_POST)) {
             switch($mode) {
                 case 'create':
                     if(empty($topicInfo)) {
-                        $topicInfo = $forumTopics->createTopic(
+                        $topicInfo = $msz->forumCtx->topics->createTopic(
                             $categoryInfo,
                             $currentUser,
                             $topicTitle,
                             $topicType
                         );
 
-                        $topicId = $topicInfo->getId();
-                        $forumCategories->incrementCategoryTopics($categoryInfo);
+                        $topicId = $topicInfo->id;
+                        $msz->forumCtx->categories->incrementCategoryTopics($categoryInfo);
                     } else
-                        $forumTopics->bumpTopic($topicInfo);
+                        $msz->forumCtx->topics->bumpTopic($topicInfo);
 
-                    $postInfo = $forumPosts->createPost(
+                    $postInfo = $msz->forumCtx->posts->createPost(
                         $topicId,
                         $currentUser,
                         $_SERVER['REMOTE_ADDR'],
@@ -229,16 +222,16 @@ if(!empty($_POST)) {
                         $categoryInfo
                     );
 
-                    $postId = $postInfo->getId();
-                    $forumCategories->incrementCategoryPosts($categoryInfo);
+                    $postId = $postInfo->id;
+                    $msz->forumCtx->categories->incrementCategoryPosts($categoryInfo);
                     break;
 
                 case 'edit':
-                    $markUpdated = $postInfo->getUserId() === $currentUserId
-                        && $postInfo->shouldMarkAsEdited()
-                        && $postText !== $postInfo->getBody();
+                    $markUpdated = $postInfo->userId === $currentUserId
+                        && $postInfo->shouldMarkAsEdited
+                        && $postText !== $postInfo->body;
 
-                    $forumPosts->updatePost(
+                    $msz->forumCtx->posts->updatePost(
                         $postId,
                         remoteAddr: $_SERVER['REMOTE_ADDR'],
                         body: $postText,
@@ -248,7 +241,7 @@ if(!empty($_POST)) {
                     );
 
                     if($isEditingTopic && ($topicTitleChanged || $topicTypeChanged))
-                        $forumTopics->updateTopic(
+                        $msz->forumCtx->topics->updateTopic(
                             $topicId,
                             title: $topicTitle,
                             type: $topicType
@@ -258,7 +251,7 @@ if(!empty($_POST)) {
 
             if(empty($notices)) {
                 // does this ternary ever return forum-topic?
-                $redirect = $msz->getUrls()->format(empty($topicInfo) ? 'forum-topic' : 'forum-post', [
+                $redirect = $msz->urls->format(empty($topicInfo) ? 'forum-topic' : 'forum-post', [
                     'topic' => $topicId ?? 0,
                     'post' => $postId ?? 0,
                 ]);
@@ -276,32 +269,32 @@ if($mode === 'edit') { // $post is pretty much sure to be populated at this poin
     $post = new stdClass;
     $post->info = $postInfo;
 
-    if($postInfo->hasUserId()) {
-        $post->user = $usersCtx->getUserInfo($postInfo->getUserId());
-        $post->colour = $usersCtx->getUserColour($post->user);
-        $post->postsCount = $forumCtx->countTotalUserPosts($post->user);
+    if($postInfo->userId !== null) {
+        $post->user = $msz->usersCtx->getUserInfo($postInfo->userId);
+        $post->colour = $msz->usersCtx->getUserColour($post->user);
+        $post->postsCount = $msz->forumCtx->countTotalUserPosts($post->user);
     }
 
-    $post->isOriginalPost = $originalPostInfo->getId() == $postInfo->getId();
-    $post->isOriginalPoster = $originalPostInfo->hasUserId() && $postInfo->hasUserId()
-        && $originalPostInfo->getUserId() === $postInfo->getUserId();
+    $post->isOriginalPost = $originalPostInfo->id == $postInfo->id;
+    $post->isOriginalPoster = $originalPostInfo->userId !== null && $postInfo->userId !== nul
+        && $originalPostInfo->userId === $postInfo->userId;
 
     Template::set('posting_post', $post);
 }
 
 try {
-    $lastPostInfo = $forumPosts->getPost(userInfo: $currentUser, getLast: true, deleted: false);
-    $selectedParser = $lastPostInfo->getParser();
+    $lastPostInfo = $msz->forumCtx->posts->getPost(userInfo: $currentUser, getLast: true, deleted: false);
+    $selectedParser = $lastPostInfo->parser;
 } catch(RuntimeException $ex) {
     $selectedParser = Parser::BBCODE;
 }
 
 Template::render('forum.posting', [
-    'posting_breadcrumbs' => iterator_to_array($forumCategories->getCategoryAncestry($categoryInfo)),
-    'global_accent_colour' => $forumCategories->getCategoryColour($categoryInfo),
+    'posting_breadcrumbs' => iterator_to_array($msz->forumCtx->categories->getCategoryAncestry($categoryInfo)),
+    'global_accent_colour' => $msz->forumCtx->categories->getCategoryColour($categoryInfo),
     'posting_user' => $currentUser,
-    'posting_user_colour' => $usersCtx->getUserColour($currentUser),
-    'posting_user_posts_count' => $forumCtx->countTotalUserPosts($currentUser),
+    'posting_user_colour' => $msz->usersCtx->getUserColour($currentUser),
+    'posting_user_posts_count' => $msz->forumCtx->countTotalUserPosts($currentUser),
     'posting_user_preferred_parser' => $selectedParser,
     'posting_forum' => $categoryInfo,
     'posting_notices' => $notices,
diff --git a/public-legacy/forum/topic.php b/public-legacy/forum/topic.php
index 81fb52ae..20796b05 100644
--- a/public-legacy/forum/topic.php
+++ b/public-legacy/forum/topic.php
@@ -4,40 +4,31 @@ namespace Misuzu;
 use stdClass;
 use RuntimeException;
 
-$urls = $msz->getUrls();
-$forumCtx = $msz->getForumContext();
-$forumCategories = $forumCtx->getCategories();
-$forumTopics = $forumCtx->getTopics();
-$forumTopicRedirects = $forumCtx->getTopicRedirects();
-$forumPosts = $forumCtx->getPosts();
-$usersCtx = $msz->getUsersContext();
-
 $postId = !empty($_GET['p']) && is_string($_GET['p']) ? (int)$_GET['p'] : 0;
 $topicId = !empty($_GET['t']) && is_string($_GET['t']) ? (int)$_GET['t'] : 0;
 $categoryId = null;
 $moderationMode = !empty($_GET['m']) && is_string($_GET['m']) ? (string)$_GET['m'] : '';
 $submissionConfirmed = !empty($_GET['confirm']) && is_string($_GET['confirm']) && $_GET['confirm'] === '1';
 
-$authInfo = $msz->getAuthInfo();
-$currentUser = $authInfo->getUserInfo();
+$currentUser = $msz->authInfo->userInfo;
 $currentUserId = $currentUser === null ? '0' : $currentUser->getId();
 
 if($topicId < 1 && $postId > 0) {
     try {
-        $postInfo = $forumPosts->getPost(postId: $postId);
+        $postInfo = $msz->forumCtx->posts->getPost(postId: $postId);
     } catch(RuntimeException $ex) {
         Template::throwError(404);
     }
 
-    $categoryId = $postInfo->getCategoryId();
-    $perms = $authInfo->getPerms('forum', $postInfo->getCategoryId());
+    $categoryId = (int)$postInfo->categoryId;
+    $perms = $msz->authInfo->getPerms('forum', $postInfo->categoryId);
     $canDeleteAny = $perms->check(Perm::F_POST_DELETE_ANY);
 
-    if($postInfo->isDeleted() && !$canDeleteAny)
+    if($postInfo->deleted && !$canDeleteAny)
         Template::throwError(404);
 
-    $topicId = $postInfo->getTopicId();
-    $preceedingPostCount = $forumPosts->countPosts(
+    $topicId = $postInfo->topicId;
+    $preceedingPostCount = $msz->forumCtx->posts->countPosts(
         topicInfo: $topicId,
         upToPostInfo: $postInfo,
         deleted: $canDeleteAny ? null : false
@@ -46,32 +37,32 @@ if($topicId < 1 && $postId > 0) {
 
 try {
     $topicIsNuked = $topicIsDeleted = $canDeleteAny = false;
-    $topicInfo = $forumTopics->getTopic(topicId: $topicId);
+    $topicInfo = $msz->forumCtx->topics->getTopic(topicId: $topicId);
 } catch(RuntimeException $ex) {
     $topicIsNuked = true;
 }
 
 if(!$topicIsNuked) {
-    $topicIsDeleted = $topicInfo->isDeleted();
+    $topicIsDeleted = $topicInfo->deleted;
 
-    if($categoryId !== (int)$topicInfo->getCategoryId()) {
-        $categoryId = (int)$topicInfo->getCategoryId();
-        $perms = $authInfo->getPerms('forum', $topicInfo->getCategoryId());
+    if($categoryId !== (int)$topicInfo->categoryId) {
+        $categoryId = (int)$topicInfo->categoryId;
+        $perms = $msz->authInfo->getPerms('forum', $topicInfo->categoryId);
     }
 
-    if($usersCtx->hasActiveBan($currentUser))
+    if($msz->usersCtx->hasActiveBan($currentUser))
         $perms = $perms->apply(fn($calc) => $calc & (Perm::F_CATEGORY_LIST | Perm::F_CATEGORY_VIEW));
 
     $canDeleteAny = $perms->check(Perm::F_POST_DELETE_ANY);
 }
 
 if($topicIsNuked || $topicIsDeleted) {
-    if($forumTopicRedirects->hasTopicRedirect($topicId)) {
-        $topicRedirectInfo = $forumTopicRedirects->getTopicRedirect($topicId);
+    if($msz->forumCtx->topicRedirects->hasTopicRedirect($topicId)) {
+        $topicRedirectInfo = $msz->forumCtx->topicRedirects->getTopicRedirect($topicId);
         Template::set('topic_redir_info', $topicRedirectInfo);
 
         if($topicIsNuked || !$canDeleteAny) {
-            header('Location: ' . $topicRedirectInfo->getLinkTarget());
+            header('Location: ' . $topicRedirectInfo->linkTarget);
             return;
         }
     }
@@ -87,10 +78,10 @@ if(!$perms->check(Perm::F_CATEGORY_VIEW))
 // this should be in the config
 $deletePostThreshold = 1;
 
-$categoryInfo = $forumCategories->getCategory(topicInfo: $topicInfo);
-$topicIsLocked = $topicInfo->isLocked();
-$topicIsArchived = $categoryInfo->isArchived();
-$topicPostsTotal = $topicInfo->getTotalPostsCount();
+$categoryInfo = $msz->forumCtx->categories->getCategory(topicInfo: $topicInfo);
+$topicIsLocked = $topicInfo->locked;
+$topicIsArchived = $categoryInfo->archived;
+$topicPostsTotal = $topicInfo->totalPostsCount;
 $topicIsFrozen = $topicIsArchived || $topicIsDeleted;
 $canDeleteOwn = !$topicIsFrozen && !$topicIsLocked && $perms->check(Perm::F_POST_DELETE_OWN);
 $canBumpTopic = !$topicIsFrozen && $perms->check(Perm::F_TOPIC_BUMP);
@@ -101,7 +92,7 @@ $canDelete = !$topicIsDeleted && (
         $topicPostsTotal > 0
         && $topicPostsTotal <= $deletePostThreshold
         && $canDeleteOwn
-        && $topicInfo->getUserId() === (string)$currentUserId
+        && $topicInfo->userId === (string)$currentUserId
     )
 );
 
@@ -114,35 +105,34 @@ if(in_array($moderationMode, $validModerationModes, true)) {
     if(!CSRF::validateRequest())
         Template::displayInfo("Couldn't verify this request, please refresh the page and try again.", 403);
 
-    $authInfo = $authInfo;
-    if(!$authInfo->isLoggedIn())
+    if(!$msz->authInfo->isLoggedIn)
         Template::displayInfo('You must be logged in to manage posts.', 401);
 
-    if($usersCtx->hasActiveBan($currentUser))
+    if($msz->usersCtx->hasActiveBan($currentUser))
         Template::displayInfo('You have been banned, check your profile for more information.', 403);
 
     switch($moderationMode) {
         case 'delete':
             if($canDeleteAny) {
-                if($topicInfo->isDeleted())
+                if($topicInfo->deleted)
                     Template::displayInfo('This topic has already been marked as deleted.', 404);
             } else {
-                if($topicInfo->isDeleted())
+                if($topicInfo->deleted)
                     Template::throwError(404);
 
                 if(!$canDeleteOwn)
                     Template::displayInfo("You aren't allowed to delete topics.", 403);
 
-                if($topicInfo->getUserId() !== $currentUser->getId())
+                if($topicInfo->userId !== $currentUser->getId())
                     Template::displayInfo('You can only delete your own topics.', 403);
 
                 // topics may only be deleted within a day of creation, this should be a config value
                 $deleteTimeFrame = 60 * 60 * 24;
-                if($topicInfo->getCreatedTime() < time() - $deleteTimeFrame)
+                if($topicInfo->createdTime < time() - $deleteTimeFrame)
                     Template::displayInfo('This topic has existed for too long. Ask a moderator to remove if it absolutely necessary.', 403);
 
                 // deleted posts are intentionally included
-                $topicPostCount = $forumPosts->countPosts(topicInfo: $topicInfo);
+                $topicPostCount = $msz->forumCtx->posts->countPosts(topicInfo: $topicInfo);
                 if($topicPostCount > $deletePostThreshold)
                     Template::displayInfo('This topic already has replies, you may no longer delete it. Ask a moderator to remove if it absolutely necessary.', 403);
             }
@@ -151,26 +141,26 @@ if(in_array($moderationMode, $validModerationModes, true)) {
                 Template::render('forum.confirm', [
                     'title' => 'Confirm topic deletion',
                     'class' => 'far fa-trash-alt',
-                    'message' => sprintf('You are about to delete topic #%d. Are you sure about that?', $topicInfo->getId()),
+                    'message' => sprintf('You are about to delete topic #%d. Are you sure about that?', $topicInfo->id),
                     'params' => [
-                        't' => $topicInfo->getId(),
+                        't' => $topicInfo->id,
                         'm' => 'delete',
                     ],
                 ]);
                 break;
             } elseif(!$submissionConfirmed) {
-                Tools::redirect($urls->format(
+                Tools::redirect($msz->urls->format(
                     'forum-topic',
-                    ['topic' => $topicInfo->getId()]
+                    ['topic' => $topicInfo->id]
                 ));
                 break;
             }
 
-            $forumTopics->deleteTopic($topicInfo->getId());
-            $msz->createAuditLog('FORUM_TOPIC_DELETE', [$topicInfo->getId()]);
+            $msz->forumCtx->topics->deleteTopic($topicInfo->id);
+            $msz->createAuditLog('FORUM_TOPIC_DELETE', [$topicInfo->id]);
 
-            Tools::redirect($urls->format('forum-category', [
-                'forum' => $categoryInfo->getId(),
+            Tools::redirect($msz->urls->format('forum-category', [
+                'forum' => $categoryInfo->id,
             ]));
             break;
 
@@ -182,25 +172,25 @@ if(in_array($moderationMode, $validModerationModes, true)) {
                 Template::render('forum.confirm', [
                     'title' => 'Confirm topic restore',
                     'class' => 'fas fa-magic',
-                    'message' => sprintf('You are about to restore topic #%d. Are you sure about that?', $topicInfo->getId()),
+                    'message' => sprintf('You are about to restore topic #%d. Are you sure about that?', $topicInfo->id),
                     'params' => [
-                        't' => $topicInfo->getId(),
+                        't' => $topicInfo->id,
                         'm' => 'restore',
                     ],
                 ]);
                 break;
             } elseif(!$submissionConfirmed) {
-                Tools::redirect($urls->format('forum-topic', [
-                    'topic' => $topicInfo->getId(),
+                Tools::redirect($msz->urls->format('forum-topic', [
+                    'topic' => $topicInfo->id,
                 ]));
                 break;
             }
 
-            $forumTopics->restoreTopic($topicInfo->getId());
-            $msz->createAuditLog('FORUM_TOPIC_RESTORE', [$topicInfo->getId()]);
+            $msz->forumCtx->topics->restoreTopic($topicInfo->id);
+            $msz->createAuditLog('FORUM_TOPIC_RESTORE', [$topicInfo->id]);
 
-            Tools::redirect($urls->format('forum-category', [
-                'forum' => $categoryInfo->getId(),
+            Tools::redirect($msz->urls->format('forum-category', [
+                'forum' => $categoryInfo->id,
             ]));
             break;
 
@@ -212,67 +202,67 @@ if(in_array($moderationMode, $validModerationModes, true)) {
                 Template::render('forum.confirm', [
                     'title' => 'Confirm topic nuke',
                     'class' => 'fas fa-radiation',
-                    'message' => sprintf('You are about to PERMANENTLY DELETE topic #%d. Are you sure about that?', $topicInfo->getId()),
+                    'message' => sprintf('You are about to PERMANENTLY DELETE topic #%d. Are you sure about that?', $topicInfo->id),
                     'params' => [
-                        't' => $topicInfo->getId(),
+                        't' => $topicInfo->id,
                         'm' => 'nuke',
                     ],
                 ]);
                 break;
             } elseif(!$submissionConfirmed) {
-                Tools::redirect($urls->format('forum-topic', [
-                    'topic' => $topicInfo->getId(),
+                Tools::redirect($msz->urls->format('forum-topic', [
+                    'topic' => $topicInfo->id,
                 ]));
                 break;
             }
 
-            $forumTopics->nukeTopic($topicInfo->getId());
-            $msz->createAuditLog('FORUM_TOPIC_NUKE', [$topicInfo->getId()]);
+            $msz->forumCtx->topics->nukeTopic($topicInfo->id);
+            $msz->createAuditLog('FORUM_TOPIC_NUKE', [$topicInfo->id]);
 
-            Tools::redirect($urls->format('forum-category', [
-                'forum' => $categoryInfo->getId(),
+            Tools::redirect($msz->urls->format('forum-category', [
+                'forum' => $categoryInfo->id,
             ]));
             break;
 
         case 'bump':
             if($canBumpTopic) {
-                $forumTopics->bumpTopic($topicInfo->getId());
-                $msz->createAuditLog('FORUM_TOPIC_BUMP', [$topicInfo->getId()]);
+                $msz->forumCtx->topics->bumpTopic($topicInfo->id);
+                $msz->createAuditLog('FORUM_TOPIC_BUMP', [$topicInfo->id]);
             }
 
-            Tools::redirect($urls->format('forum-topic', [
-                'topic' => $topicInfo->getId(),
+            Tools::redirect($msz->urls->format('forum-topic', [
+                'topic' => $topicInfo->id,
             ]));
             break;
 
         case 'lock':
             if($canLockTopic && !$topicIsLocked) {
-                $forumTopics->lockTopic($topicInfo->getId());
-                $msz->createAuditLog('FORUM_TOPIC_LOCK', [$topicInfo->getId()]);
+                $msz->forumCtx->topics->lockTopic($topicInfo->id);
+                $msz->createAuditLog('FORUM_TOPIC_LOCK', [$topicInfo->id]);
             }
 
-            Tools::redirect($urls->format('forum-topic', [
-                'topic' => $topicInfo->getId(),
+            Tools::redirect($msz->urls->format('forum-topic', [
+                'topic' => $topicInfo->id,
             ]));
             break;
 
         case 'unlock':
             if($canLockTopic && $topicIsLocked) {
-                $forumTopics->unlockTopic($topicInfo->getId());
-                $msz->createAuditLog('FORUM_TOPIC_UNLOCK', [$topicInfo->getId()]);
+                $msz->forumCtx->topics->unlockTopic($topicInfo->id);
+                $msz->createAuditLog('FORUM_TOPIC_UNLOCK', [$topicInfo->id]);
             }
 
-            Tools::redirect($urls->format('forum-topic', [
-                'topic' => $topicInfo->getId(),
+            Tools::redirect($msz->urls->format('forum-topic', [
+                'topic' => $topicInfo->id,
             ]));
             break;
     }
     return;
 }
 
-$topicPosts = $topicInfo->getPostsCount();
+$topicPosts = $topicInfo->postsCount;
 if($canDeleteAny)
-    $topicPosts += $topicInfo->getDeletedPostsCount();
+    $topicPosts += $topicInfo->deletedPostsCount;
 
 $topicPagination = new Pagination($topicPosts, 10, 'page');
 
@@ -282,7 +272,7 @@ if(isset($preceedingPostCount))
 if(!$topicPagination->hasValidOffset())
     Template::throwError(404);
 
-$postInfos = $forumPosts->getPosts(
+$postInfos = $msz->forumCtx->posts->getPosts(
     topicInfo: $topicInfo,
     deleted: $perms->check(Perm::F_POST_DELETE_ANY) ? null : false,
     pagination: $topicPagination,
@@ -292,7 +282,7 @@ if(empty($postInfos))
     Template::throwError(404);
 
 try {
-    $originalPostInfo = $forumPosts->getPost(topicInfo: $topicInfo);
+    $originalPostInfo = $msz->forumCtx->posts->getPost(topicInfo: $topicInfo);
 } catch(RuntimeException $ex) {
     Template::throwError(404);
 }
@@ -303,23 +293,23 @@ foreach($postInfos as $postInfo) {
     $posts[] = $post = new stdClass;
     $post->info = $postInfo;
 
-    if($postInfo->hasUserId()) {
-        $post->user = $usersCtx->getUserInfo($postInfo->getUserId());
-        $post->colour = $usersCtx->getUserColour($post->user);
-        $post->postsCount = $forumCtx->countTotalUserPosts($post->user);
+    if($postInfo->userId !== null) {
+        $post->user = $msz->usersCtx->getUserInfo($postInfo->userId);
+        $post->colour = $msz->usersCtx->getUserColour($post->user);
+        $post->postsCount = $msz->forumCtx->countTotalUserPosts($post->user);
     }
 
-    $post->isOriginalPost = $originalPostInfo->getId() == $postInfo->getId();
-    $post->isOriginalPoster = $originalPostInfo->hasUserId() && $postInfo->hasUserId()
-        && $originalPostInfo->getUserId() === $postInfo->getUserId();
+    $post->isOriginalPost = $originalPostInfo->id == $postInfo->id;
+    $post->isOriginalPoster = $originalPostInfo->userId !== null && $postInfo->userId !== null
+        && $originalPostInfo->userId === $postInfo->userId;
 }
 
 $canReply = !$topicIsArchived && !$topicIsLocked && !$topicIsDeleted && $perms->check(Perm::F_POST_CREATE);
 
-if(!$forumTopics->checkUserHasReadTopic($currentUser, $topicInfo))
-    $forumTopics->incrementTopicViews($topicInfo);
+if(!$msz->forumCtx->topics->checkUserHasReadTopic($currentUser, $topicInfo))
+    $msz->forumCtx->topics->incrementTopicViews($topicInfo);
 
-$forumTopics->updateUserReadTopic($currentUser, $topicInfo);
+$msz->forumCtx->topics->updateUserReadTopic($currentUser, $topicInfo);
 
 $perms = $perms->checkMany([
     'can_create_post' => Perm::F_POST_CREATE,
@@ -330,8 +320,8 @@ $perms = $perms->checkMany([
 ]);
 
 Template::render('forum.topic', [
-    'topic_breadcrumbs' => iterator_to_array($forumCategories->getCategoryAncestry($topicInfo)),
-    'global_accent_colour' => $forumCategories->getCategoryColour($topicInfo),
+    'topic_breadcrumbs' => iterator_to_array($msz->forumCtx->categories->getCategoryAncestry($topicInfo)),
+    'global_accent_colour' => $msz->forumCtx->categories->getCategoryColour($topicInfo),
     'topic_info' => $topicInfo,
     'category_info' => $categoryInfo,
     'topic_posts' => $posts,
diff --git a/public-legacy/manage/changelog/change.php b/public-legacy/manage/changelog/change.php
index b3ac9b4d..4cb9ee40 100644
--- a/public-legacy/manage/changelog/change.php
+++ b/public-legacy/manage/changelog/change.php
@@ -7,28 +7,25 @@ use Misuzu\Changelog\Changelog;
 use Carbon\CarbonImmutable;
 use Index\{XArray,XDateTime};
 
-$authInfo = $msz->getAuthInfo();
-if(!$authInfo->getPerms('global')->check(Perm::G_CL_CHANGES_MANAGE))
+if(!$msz->authInfo->getPerms('global')->check(Perm::G_CL_CHANGES_MANAGE))
     Template::throwError(403);
 
 $changeActions = [];
 foreach(Changelog::ACTIONS as $action)
     $changeActions[$action] = Changelog::actionText($action);
 
-$urls = $msz->getUrls();
-$changelog = $msz->getChangelog();
 $changeId = (string)filter_input(INPUT_GET, 'c', FILTER_SANITIZE_NUMBER_INT);
 $changeInfo = null;
 $changeTagIds = [];
-$tagInfos = $changelog->getTags();
+$tagInfos = $msz->changelog->getTags();
 
 if(empty($changeId))
     $isNew = true;
 else
     try {
         $isNew = false;
-        $changeInfo = $changelog->getChange($changeId);
-        $changeTagIds = XArray::select($changelog->getTags(changeInfo: $changeInfo), fn($tagInfo) => $tagInfo->getId());
+        $changeInfo = $msz->changelog->getChange($changeId);
+        $changeTagIds = XArray::select($msz->changelog->getTags(changeInfo: $changeInfo), fn($tagInfo) => $tagInfo->id);
     } catch(RuntimeException $ex) {
         Template::throwError(404);
     }
@@ -37,9 +34,9 @@ if($_SERVER['REQUEST_METHOD'] === 'GET' && !empty($_GET['delete'])) {
     if(!CSRF::validateRequest())
         Template::throwError(403);
 
-    $changelog->deleteChange($changeInfo);
-    $msz->createAuditLog('CHANGELOG_ENTRY_DELETE', [$changeInfo->getId()]);
-    Tools::redirect($urls->format('manage-changelog-changes'));
+    $msz->changelog->deleteChange($changeInfo);
+    $msz->createAuditLog('CHANGELOG_ENTRY_DELETE', [$changeInfo->id]);
+    Tools::redirect($msz->urls->format('manage-changelog-changes'));
     return;
 }
 
@@ -64,20 +61,20 @@ while($_SERVER['REQUEST_METHOD'] === 'POST' && CSRF::validateRequest()) {
     }
 
     if($isNew) {
-        $changeInfo = $changelog->createChange($action, $summary, $body, $userId, $createdAt);
+        $changeInfo = $msz->changelog->createChange($action, $summary, $body, $userId, $createdAt);
     } else {
-        if($action === $changeInfo->getAction())
+        if($action === $changeInfo->action)
             $action = null;
-        if($summary === $changeInfo->getSummary())
+        if($summary === $changeInfo->summary)
             $summary = null;
-        if($body === $changeInfo->getBody())
+        if($body === $changeInfo->body)
             $body = null;
-        if($createdAt !== null && XDateTime::compare($createdAt, $changeInfo->getCreatedAt()) === 0)
+        if($createdAt !== null && XDateTime::compare($createdAt, $changeInfo->createdAt) === 0)
             $createdAt = null;
-        $updateUserInfo = $userId !== $changeInfo->getUserId();
+        $updateUserInfo = $userId !== $changeInfo->userId;
 
         if($action !== null || $summary !== null || $body !== null || $createdAt !== null || $updateUserInfo)
-            $changelog->updateChange($changeInfo, $action, $summary, $body, $updateUserInfo, $userId, $createdAt);
+            $msz->changelog->updateChange($changeInfo, $action, $summary, $body, $updateUserInfo, $userId, $createdAt);
     }
 
     if(!empty($tags)) {
@@ -88,24 +85,24 @@ while($_SERVER['REQUEST_METHOD'] === 'POST' && CSRF::validateRequest()) {
         foreach($tCurrent as $tag)
             if(!in_array($tag, $tApply)) {
                 $tRemove[] = $tag;
-                $changelog->removeTagFromChange($changeInfo, $tag);
+                $msz->changelog->removeTagFromChange($changeInfo, $tag);
             }
 
         $tCurrent = array_diff($tCurrent, $tRemove);
 
         foreach($tApply as $tag)
             if(!in_array($tag, $tCurrent)) {
-                $changelog->addTagToChange($changeInfo, $tag);
+                $msz->changelog->addTagToChange($changeInfo, $tag);
                 $tCurrent[] = $tag;
             }
     }
 
     $msz->createAuditLog(
         $isNew ? 'CHANGELOG_ENTRY_CREATE' : 'CHANGELOG_ENTRY_EDIT',
-        [$changeInfo->getId()]
+        [$changeInfo->id]
     );
 
-    Tools::redirect($urls->format('manage-changelog-change', ['change' => $changeInfo->getId()]));
+    Tools::redirect($msz->urls->format('manage-changelog-change', ['change' => $changeInfo->id]));
     return;
 }
 
@@ -115,5 +112,5 @@ Template::render('manage.changelog.change', [
     'change_info_tags' => $changeTagIds,
     'change_tags' => $tagInfos,
     'change_actions' => $changeActions,
-    'change_author_id' => $authInfo->getUserInfo()->getId(),
+    'change_author_id' => $msz->authInfo->userId,
 ]);
diff --git a/public-legacy/manage/changelog/index.php b/public-legacy/manage/changelog/index.php
index 6ad8c3df..ae50751b 100644
--- a/public-legacy/manage/changelog/index.php
+++ b/public-legacy/manage/changelog/index.php
@@ -3,32 +3,28 @@ namespace Misuzu;
 
 use RuntimeException;
 
-if(!$msz->getAuthInfo()->getPerms('global')->check(Perm::G_CL_CHANGES_MANAGE))
+if(!$msz->authInfo->getPerms('global')->check(Perm::G_CL_CHANGES_MANAGE))
     Template::throwError(403);
 
-$changelog = $msz->getChangelog();
-$usersCtx = $msz->getUsersContext();
-
-$changelogPagination = new Pagination($changelog->countChanges(), 30);
-
-if(!$changelogPagination->hasValidOffset())
+$pagination = new Pagination($msz->changelog->countChanges(), 30);
+if(!$pagination->hasValidOffset())
     Template::throwError(404);
 
-$changeInfos = $changelog->getChanges(pagination: $changelogPagination);
+$changeInfos = $msz->changelog->getChanges(pagination: $pagination);
 $changes = [];
 
 foreach($changeInfos as $changeInfo) {
-    $userInfo = $changeInfo->hasUserId() ? $usersCtx->getUserInfo($changeInfo->getUserId()) : null;
+    $userInfo = $changeInfo->userId !== null ? $msz->usersCtx->getUserInfo($changeInfo->userId) : null;
 
     $changes[] = [
         'change' => $changeInfo,
-        'tags' => $changelog->getTags(changeInfo: $changeInfo),
+        'tags' => $msz->changelog->getTags(changeInfo: $changeInfo),
         'user' => $userInfo,
-        'user_colour' => $usersCtx->getUserColour($userInfo),
+        'user_colour' => $msz->usersCtx->getUserColour($userInfo),
     ];
 }
 
 Template::render('manage.changelog.changes', [
     'changelog_changes' => $changes,
-    'changelog_pagination' => $changelogPagination,
+    'changelog_pagination' => $pagination,
 ]);
diff --git a/public-legacy/manage/changelog/tag.php b/public-legacy/manage/changelog/tag.php
index 9983604c..7fe747c0 100644
--- a/public-legacy/manage/changelog/tag.php
+++ b/public-legacy/manage/changelog/tag.php
@@ -3,13 +3,11 @@ namespace Misuzu;
 
 use RuntimeException;
 
-if(!$msz->getAuthInfo()->getPerms('global')->check(Perm::G_CL_TAGS_MANAGE))
+if(!$msz->authInfo->getPerms('global')->check(Perm::G_CL_TAGS_MANAGE))
     Template::throwError(403);
 
-$urls = $msz->getUrls();
-$changelog = $msz->getChangelog();
 $tagId = (string)filter_input(INPUT_GET, 't', FILTER_SANITIZE_NUMBER_INT);
-$loadTagInfo = fn() => $changelog->getTag($tagId);
+$loadTagInfo = fn() => $msz->changelog->getTag($tagId);
 
 if(empty($tagId))
     $isNew = true;
@@ -25,9 +23,9 @@ if($_SERVER['REQUEST_METHOD'] === 'GET' && !empty($_GET['delete'])) {
     if(!CSRF::validateRequest())
         Template::throwError(403);
 
-    $changelog->deleteTag($tagInfo);
-    $msz->createAuditLog('CHANGELOG_TAG_DELETE', [$tagInfo->getId()]);
-    Tools::redirect($urls->format('manage-changelog-tags'));
+    $msz->changelog->deleteTag($tagInfo);
+    $msz->createAuditLog('CHANGELOG_TAG_DELETE', [$tagInfo->id]);
+    Tools::redirect($msz->urls->format('manage-changelog-tags'));
     return;
 }
 
@@ -37,26 +35,26 @@ while($_SERVER['REQUEST_METHOD'] === 'POST' && CSRF::validateRequest()) {
     $archive = !empty($_POST['ct_archive']);
 
     if($isNew) {
-        $tagInfo = $changelog->createTag($name, $description, $archive);
+        $tagInfo = $msz->changelog->createTag($name, $description, $archive);
     } else {
-        if($name === $tagInfo->getName())
+        if($name === $tagInfo->name)
             $name = null;
-        if($description === $tagInfo->getDescription())
+        if($description === $tagInfo->description)
             $description = null;
-        if($archive === $tagInfo->isArchived())
+        if($archive === $tagInfo->archived)
             $archive = null;
 
         if($name !== null || $description !== null || $archive !== null)
-            $changelog->updateTag($tagInfo, $name, $description, $archive);
+            $msz->changelog->updateTag($tagInfo, $name, $description, $archive);
     }
 
     $msz->createAuditLog(
         $isNew ? 'CHANGELOG_TAG_CREATE' : 'CHANGELOG_TAG_EDIT',
-        [$tagInfo->getId()]
+        [$tagInfo->id]
     );
 
     if($isNew) {
-        Tools::redirect($urls->format('manage-changelog-tag', ['tag' => $tagInfo->getId()]));
+        Tools::redirect($msz->urls->format('manage-changelog-tag', ['tag' => $tagInfo->id]));
         return;
     } else $tagInfo = $loadTagInfo();
     break;
diff --git a/public-legacy/manage/changelog/tags.php b/public-legacy/manage/changelog/tags.php
index 2503b612..f194249c 100644
--- a/public-legacy/manage/changelog/tags.php
+++ b/public-legacy/manage/changelog/tags.php
@@ -1,9 +1,9 @@
 <?php
 namespace Misuzu;
 
-if(!$msz->getAuthInfo()->getPerms('global')->check(Perm::G_CL_TAGS_MANAGE))
+if(!$msz->authInfo->getPerms('global')->check(Perm::G_CL_TAGS_MANAGE))
     Template::throwError(403);
 
 Template::render('manage.changelog.tags', [
-    'changelog_tags' => $msz->getChangelog()->getTags(),
+    'changelog_tags' => $msz->changelog->getTags(),
 ]);
diff --git a/public-legacy/manage/forum/index.php b/public-legacy/manage/forum/index.php
index 3371eda9..6272886c 100644
--- a/public-legacy/manage/forum/index.php
+++ b/public-legacy/manage/forum/index.php
@@ -3,11 +3,10 @@ namespace Misuzu;
 
 use Misuzu\Perm;
 
-if(!$msz->getAuthInfo()->getPerms('global')->check(Perm::G_FORUM_CATEGORIES_MANAGE))
+if(!$msz->authInfo->getPerms('global')->check(Perm::G_FORUM_CATEGORIES_MANAGE))
     Template::throwError(403);
 
-$perms = $msz->getPerms();
-$permsInfos = $perms->getPermissionInfo(categoryNames: Perm::INFO_FOR_FORUM_CATEGORY);
+$permsInfos = $msz->perms->getPermissionInfo(categoryNames: Perm::INFO_FOR_FORUM_CATEGORY);
 $permsLists = Perm::createList(Perm::LISTS_FOR_FORUM_CATEGORY);
 
 if(filter_has_var(INPUT_POST, 'perms'))
diff --git a/public-legacy/manage/forum/redirs.php b/public-legacy/manage/forum/redirs.php
index 91735e42..b9decf46 100644
--- a/public-legacy/manage/forum/redirs.php
+++ b/public-legacy/manage/forum/redirs.php
@@ -1,14 +1,9 @@
 <?php
 namespace Misuzu;
 
-$authInfo = $msz->getAuthInfo();
-if(!$authInfo->getPerms('global')->check(Perm::G_FORUM_TOPIC_REDIRS_MANAGE))
+if(!$msz->authInfo->getPerms('global')->check(Perm::G_FORUM_TOPIC_REDIRS_MANAGE))
     Template::throwError(403);
 
-$urls = $msz->getUrls();
-$forumCtx = $msz->getForumContext();
-$forumTopicRedirects = $forumCtx->getTopicRedirects();
-
 if($_SERVER['REQUEST_METHOD'] === 'POST') {
     if(!CSRF::validateRequest())
         throw new \Exception("Request verification failed.");
@@ -17,8 +12,8 @@ if($_SERVER['REQUEST_METHOD'] === 'POST') {
     $rTopicURL = trim((string)filter_input(INPUT_POST, 'topic_redir_url'));
 
     $msz->createAuditLog('FORUM_TOPIC_REDIR_CREATE', [$rTopicId]);
-    $forumTopicRedirects->createTopicRedirect($rTopicId, $authInfo->getUserInfo(), $rTopicURL);
-    Tools::redirect($urls->format('manage-forum-topic-redirs'));
+    $msz->forumCtx->topicRedirects->createTopicRedirect($rTopicId, $msz->authInfo->userInfo, $rTopicURL);
+    Tools::redirect($msz->urls->format('manage-forum-topic-redirs'));
     return;
 }
 
@@ -28,16 +23,16 @@ if(filter_input(INPUT_GET, 'm') === 'explode') {
 
     $rTopicId = (string)filter_input(INPUT_GET, 't');
     $msz->createAuditLog('FORUM_TOPIC_REDIR_REMOVE', [$rTopicId]);
-    $forumTopicRedirects->deleteTopicRedirect($rTopicId);
-    Tools::redirect($urls->format('manage-forum-topic-redirs'));
+    $msz->forumCtx->topicRedirects->deleteTopicRedirect($rTopicId);
+    Tools::redirect($msz->urls->format('manage-forum-topic-redirs'));
     return;
 }
 
-$pagination = new Pagination($forumTopicRedirects->countTopicRedirects(), 20);
+$pagination = new Pagination($msz->forumCtx->topicRedirects->countTopicRedirects(), 20);
 if(!$pagination->hasValidOffset())
     Template::throwError(404);
 
-$redirs = $forumTopicRedirects->getTopicRedirects(pagination: $pagination);
+$redirs = $msz->forumCtx->topicRedirects->getTopicRedirects(pagination: $pagination);
 
 Template::render('manage.forum.redirs', [
     'manage_redirs' => $redirs,
diff --git a/public-legacy/manage/general/emoticon.php b/public-legacy/manage/general/emoticon.php
index fb8faa4e..2ec66ff8 100644
--- a/public-legacy/manage/general/emoticon.php
+++ b/public-legacy/manage/general/emoticon.php
@@ -4,10 +4,9 @@ namespace Misuzu;
 use RuntimeException;
 use Index\XArray;
 
-if(!$msz->getAuthInfo()->getPerms('global')->check(Perm::G_EMOTES_MANAGE))
+if(!$msz->authInfo->getPerms('global')->check(Perm::G_EMOTES_MANAGE))
     Template::throwError(403);
 
-$emotes = $msz->getEmotes();
 $emoteId = (string)filter_input(INPUT_GET, 'e', FILTER_SANITIZE_NUMBER_INT);
 $emoteInfo = [];
 $emoteStrings = [];
@@ -17,8 +16,8 @@ if(empty($emoteId))
 else
     try {
         $isNew = false;
-        $emoteInfo = $emotes->getEmote($emoteId);
-        $emoteStrings = iterator_to_array($emotes->getEmoteStrings($emoteInfo));
+        $emoteInfo = $msz->emotes->getEmote($emoteId);
+        $emoteStrings = iterator_to_array($msz->emotes->getEmoteStrings($emoteInfo));
     } catch(RuntimeException $ex) {
         Template::throwError(404);
     }
@@ -30,8 +29,8 @@ while($_SERVER['REQUEST_METHOD'] === 'POST' && CSRF::validateRequest()) {
     $url = trim((string)filter_input(INPUT_POST, 'em_url'));
     $strings = explode(' ', trim((string)filter_input(INPUT_POST, 'em_strings')));
 
-    if($isNew || $url !== $emoteInfo->getUrl()) {
-        $checkUrl = $emotes->checkEmoteUrl($url);
+    if($isNew || $url !== $emoteInfo->url) {
+        $checkUrl = $msz->emotes->checkEmoteUrl($url);
         if($checkUrl !== '') {
             echo match($checkUrl) {
                 'empty' => 'URL may not be empty.',
@@ -47,36 +46,36 @@ while($_SERVER['REQUEST_METHOD'] === 'POST' && CSRF::validateRequest()) {
         $order = null;
 
     if($isNew) {
-        $emoteInfo = $emotes->createEmote($url, $minRank, $order);
+        $emoteInfo = $msz->emotes->createEmote($url, $minRank, $order);
     } else {
-        if($order === $emoteInfo->getOrder())
+        if($order === $emoteInfo->order)
             $order = null;
-        if($minRank === $emoteInfo->getMinRank())
+        if($minRank === $emoteInfo->minRank)
             $minRank = null;
-        if($url === $emoteInfo->getUrl())
+        if($url === $emoteInfo->url)
             $url = null;
 
         if($order !== null || $minRank !== null || $url !== null)
-            $emotes->updateEmote($emoteInfo, $order, $minRank, $url);
+            $msz->emotes->updateEmote($emoteInfo, $order, $minRank, $url);
     }
 
-    $sCurrent = XArray::select($emoteStrings, fn($stringInfo) => $stringInfo->getString());
+    $sCurrent = XArray::select($emoteStrings, fn($stringInfo) => $stringInfo->string);
     $sApply = $strings;
     $sRemove = [];
 
     foreach($sCurrent as $string)
         if(!in_array($string, $sApply)) {
             $sRemove[] = $string;
-            $emotes->removeEmoteString($string);
+            $msz->emotes->removeEmoteString($string);
         }
 
     $sCurrent = array_diff($sCurrent, $sRemove);
 
     foreach($sApply as $string)
         if(!in_array($string, $sCurrent)) {
-            $checkString = $emotes->checkEmoteString($string);
+            $checkString = $msz->emotes->checkEmoteString($string);
             if($checkString === '') {
-                $emotes->addEmoteString($emoteInfo, $string);
+                $msz->emotes->addEmoteString($emoteInfo, $string);
             } else {
                 echo match($checkString) {
                     'empty' => 'String may not be empty.',
@@ -94,10 +93,10 @@ while($_SERVER['REQUEST_METHOD'] === 'POST' && CSRF::validateRequest()) {
 
     $msz->createAuditLog(
         $isNew ? 'EMOTICON_CREATE' : 'EMOTICON_EDIT',
-        [$emoteInfo->getId()]
+        [$emoteInfo->id]
     );
 
-    Tools::redirect($msz->getUrls()->format('manage-general-emoticon', ['emote' => $emoteInfo->getId()]));
+    Tools::redirect($msz->urls->format('manage-general-emoticon', ['emote' => $emoteInfo->id]));
     return;
 }
 
diff --git a/public-legacy/manage/general/emoticons.php b/public-legacy/manage/general/emoticons.php
index a17488d2..c60615c9 100644
--- a/public-legacy/manage/general/emoticons.php
+++ b/public-legacy/manage/general/emoticons.php
@@ -3,44 +3,42 @@ namespace Misuzu;
 
 use RuntimeException;
 
-if(!$msz->getAuthInfo()->getPerms('global')->check(Perm::G_EMOTES_MANAGE))
+if(!$msz->authInfo->getPerms('global')->check(Perm::G_EMOTES_MANAGE))
     Template::throwError(403);
 
-$emotes = $msz->getEmotes();
-
 if(CSRF::validateRequest() && !empty($_GET['emote'])) {
     $emoteId = (string)filter_input(INPUT_GET, 'emote', FILTER_SANITIZE_NUMBER_INT);
 
     try {
-        $emoteInfo = $emotes->getEmote($emoteId);
+        $emoteInfo = $msz->emotes->getEmote($emoteId);
     } catch(RuntimeException $ex) {
         Template::throwError(404);
     }
 
     if(!empty($_GET['delete'])) {
-        $emotes->deleteEmote($emoteInfo);
-        $msz->createAuditLog('EMOTICON_DELETE', [$emoteInfo->getId()]);
+        $msz->emotes->deleteEmote($emoteInfo);
+        $msz->createAuditLog('EMOTICON_DELETE', [$emoteInfo->id]);
     } else {
         if(isset($_GET['order'])) {
             $order = filter_input(INPUT_GET, 'order');
             $offset = $order === 'i' ? 10 : ($order === 'd' ? -10 : 0);
-            $emotes->updateEmoteOrderOffset($emoteInfo, $offset);
-            $msz->createAuditLog('EMOTICON_ORDER', [$emoteInfo->getId()]);
+            $msz->emotes->updateEmoteOrderOffset($emoteInfo, $offset);
+            $msz->createAuditLog('EMOTICON_ORDER', [$emoteInfo->id]);
         }
 
         if(isset($_GET['alias'])) {
             $alias = (string)filter_input(INPUT_GET, 'alias');
-            if($emotes->checkEmoteString($alias) === '') {
-                $emotes->addEmoteString($emoteInfo, $alias);
-                $msz->createAuditLog('EMOTICON_ALIAS', [$emoteInfo->getId(), $alias]);
+            if($msz->emotes->checkEmoteString($alias) === '') {
+                $msz->emotes->addEmoteString($emoteInfo, $alias);
+                $msz->createAuditLog('EMOTICON_ALIAS', [$emoteInfo->id, $alias]);
             }
         }
     }
 
-    Tools::redirect($msz->getUrls()->format('manage-general-emoticons'));
+    Tools::redirect($msz->urls->format('manage-general-emoticons'));
     return;
 }
 
 Template::render('manage.general.emoticons', [
-    'emotes' => $emotes->getEmotes(),
+    'emotes' => $msz->emotes->getEmotes(),
 ]);
diff --git a/public-legacy/manage/general/index.php b/public-legacy/manage/general/index.php
index 5c3139d3..c9f1969d 100644
--- a/public-legacy/manage/general/index.php
+++ b/public-legacy/manage/general/index.php
@@ -1,8 +1,8 @@
 <?php
 namespace Misuzu;
 
-$counterInfos = $msz->getCounters()->getCounters(orderBy: 'name');
-$counterNamesRaw = $msz->getConfig()->getArray('counters.names');
+$counterInfos = $msz->counters->getCounters(orderBy: 'name');
+$counterNamesRaw = $msz->config->getArray('counters.names');
 $counterNamesCount = count($counterNamesRaw);
 $counterNames = [];
 
diff --git a/public-legacy/manage/general/logs.php b/public-legacy/manage/general/logs.php
index d3b76a2b..b5d10291 100644
--- a/public-legacy/manage/general/logs.php
+++ b/public-legacy/manage/general/logs.php
@@ -3,26 +3,24 @@ namespace Misuzu;
 
 use Misuzu\Pagination;
 
-if(!$msz->getAuthInfo()->getPerms('global')->check(Perm::G_LOGS_VIEW))
+if(!$msz->authInfo->getPerms('global')->check(Perm::G_LOGS_VIEW))
     Template::throwError(403);
 
-$usersCtx = $msz->getUsersContext();
-$auditLog = $msz->getAuditLog();
-$pagination = new Pagination($auditLog->countLogs(), 50);
+$pagination = new Pagination($msz->auditLog->countLogs(), 50);
 
 if(!$pagination->hasValidOffset())
     Template::throwError(404);
 
-$logs = iterator_to_array($auditLog->getLogs(pagination: $pagination));
+$logs = iterator_to_array($msz->auditLog->getLogs(pagination: $pagination));
 $userInfos = [];
 $userColours = [];
 
 foreach($logs as $log)
-    if($log->hasUserId()) {
-        $userId = $log->getUserId();
+    if($log->userId !== null) {
+        $userId = $log->userId;
         if(!array_key_exists($userId, $userInfos)) {
-            $userInfos[$userId] = $usersCtx->getUserInfo($userId);
-            $userColours[$userId] = $usersCtx->getUserColour($userId);
+            $userInfos[$userId] = $msz->usersCtx->getUserInfo($userId);
+            $userColours[$userId] = $msz->usersCtx->getUserColour($userId);
         }
     }
 
diff --git a/public-legacy/manage/general/setting-delete.php b/public-legacy/manage/general/setting-delete.php
index 4f5c8183..1e8ad7aa 100644
--- a/public-legacy/manage/general/setting-delete.php
+++ b/public-legacy/manage/general/setting-delete.php
@@ -1,7 +1,7 @@
 <?php
 namespace Misuzu;
 
-if(!$msz->getAuthInfo()->getPerms('global')->check(Perm::G_CONFIG_MANAGE))
+if(!$msz->authInfo->getPerms('global')->check(Perm::G_CONFIG_MANAGE))
     Template::throwError(403);
 
 $valueName = (string)filter_input(INPUT_GET, 'name');
@@ -13,7 +13,7 @@ if($_SERVER['REQUEST_METHOD'] === 'POST' && CSRF::validateRequest()) {
     $valueName = $valueInfo->getName();
     $msz->createAuditLog('CONFIG_DELETE', [$valueName]);
     $cfg->removeValues($valueName);
-    Tools::redirect($msz->getUrls()->format('manage-general-settings'));
+    Tools::redirect($msz->urls->format('manage-general-settings'));
     return;
 }
 
diff --git a/public-legacy/manage/general/setting.php b/public-legacy/manage/general/setting.php
index 617a0580..1ed83fd0 100644
--- a/public-legacy/manage/general/setting.php
+++ b/public-legacy/manage/general/setting.php
@@ -3,7 +3,7 @@ namespace Misuzu;
 
 use Index\Config\Db\DbConfig;
 
-if(!$msz->getAuthInfo()->getPerms('global')->check(Perm::G_CONFIG_MANAGE))
+if(!$msz->authInfo->getPerms('global')->check(Perm::G_CONFIG_MANAGE))
     Template::throwError(403);
 
 $isNew = true;
@@ -73,7 +73,7 @@ while($_SERVER['REQUEST_METHOD'] === 'POST' && CSRF::validateRequest()) {
 
     $msz->createAuditLog($isNew ? 'CONFIG_CREATE' : 'CONFIG_UPDATE', [$sName]);
     $applyFunc($sName, $sValue);
-    Tools::redirect($msz->getUrls()->format('manage-general-settings'));
+    Tools::redirect($msz->urls->format('manage-general-settings'));
     return;
 }
 
diff --git a/public-legacy/manage/general/settings.php b/public-legacy/manage/general/settings.php
index 71a396c0..a93baeb6 100644
--- a/public-legacy/manage/general/settings.php
+++ b/public-legacy/manage/general/settings.php
@@ -1,7 +1,7 @@
 <?php
 namespace Misuzu;
 
-if(!$msz->getAuthInfo()->getPerms('global')->check(Perm::G_CONFIG_MANAGE))
+if(!$msz->authInfo->getPerms('global')->check(Perm::G_CONFIG_MANAGE))
     Template::throwError(403);
 
 $hidden = $cfg->getArray('settings.hidden');
diff --git a/public-legacy/manage/news/categories.php b/public-legacy/manage/news/categories.php
index ae2de450..64e42edb 100644
--- a/public-legacy/manage/news/categories.php
+++ b/public-legacy/manage/news/categories.php
@@ -1,16 +1,15 @@
 <?php
 namespace Misuzu;
 
-if(!$msz->getAuthInfo()->getPerms('global')->check(Perm::G_NEWS_CATEGORIES_MANAGE))
+if(!$msz->authInfo->getPerms('global')->check(Perm::G_NEWS_CATEGORIES_MANAGE))
     Template::throwError(403);
 
-$news = $msz->getNews();
-$pagination = new Pagination($news->countCategories(), 15);
+$pagination = new Pagination($msz->news->countCategories(), 15);
 
 if(!$pagination->hasValidOffset())
     Template::throwError(404);
 
-$categories = $news->getCategories(pagination: $pagination);
+$categories = $msz->news->getCategories(pagination: $pagination);
 
 Template::render('manage.news.categories', [
     'news_categories' => $categories,
diff --git a/public-legacy/manage/news/category.php b/public-legacy/manage/news/category.php
index 978a2e5a..b4623ea6 100644
--- a/public-legacy/manage/news/category.php
+++ b/public-legacy/manage/news/category.php
@@ -3,13 +3,11 @@ namespace Misuzu;
 
 use RuntimeException;
 
-if(!$msz->getAuthInfo()->getPerms('global')->check(Perm::G_NEWS_CATEGORIES_MANAGE))
+if(!$msz->authInfo->getPerms('global')->check(Perm::G_NEWS_CATEGORIES_MANAGE))
     Template::throwError(403);
 
-$urls = $msz->getUrls();
-$news = $msz->getNews();
 $categoryId = (string)filter_input(INPUT_GET, 'c', FILTER_SANITIZE_NUMBER_INT);
-$loadCategoryInfo = fn() => $news->getCategory(categoryId: $categoryId);
+$loadCategoryInfo = fn() => $msz->news->getCategory(categoryId: $categoryId);
 
 if(empty($categoryId))
     $isNew = true;
@@ -25,9 +23,9 @@ if($_SERVER['REQUEST_METHOD'] === 'GET' && !empty($_GET['delete'])) {
     if(!CSRF::validateRequest())
         Template::throwError(403);
 
-    $news->deleteCategory($categoryInfo);
-    $msz->createAuditLog('NEWS_CATEGORY_DELETE', [$categoryInfo->getId()]);
-    Tools::redirect($urls->format('manage-news-categories'));
+    $msz->news->deleteCategory($categoryInfo);
+    $msz->createAuditLog('NEWS_CATEGORY_DELETE', [$categoryInfo->id]);
+    Tools::redirect($msz->urls->format('manage-news-categories'));
     return;
 }
 
@@ -37,26 +35,26 @@ while($_SERVER['REQUEST_METHOD'] === 'POST' && CSRF::validateRequest()) {
     $hidden = !empty($_POST['nc_hidden']);
 
     if($isNew) {
-        $categoryInfo = $news->createCategory($name, $description, $hidden);
+        $categoryInfo = $msz->news->createCategory($name, $description, $hidden);
     } else {
-        if($name === $categoryInfo->getName())
+        if($name === $categoryInfo->name)
             $name = null;
-        if($description === $categoryInfo->getDescription())
+        if($description === $categoryInfo->description)
             $description = null;
-        if($hidden === $categoryInfo->isHidden())
+        if($hidden === $categoryInfo->hidden)
             $hidden = null;
 
         if($name !== null || $description !== null || $hidden !== null)
-            $news->updateCategory($categoryInfo, $name, $description, $hidden);
+            $msz->news->updateCategory($categoryInfo, $name, $description, $hidden);
     }
 
     $msz->createAuditLog(
         $isNew ? 'NEWS_CATEGORY_CREATE' : 'NEWS_CATEGORY_EDIT',
-        [$categoryInfo->getId()]
+        [$categoryInfo->id]
     );
 
     if($isNew) {
-        Tools::redirect($urls->format('manage-news-category', ['category' => $categoryInfo->getId()]));
+        Tools::redirect($msz->urls->format('manage-news-category', ['category' => $categoryInfo->id]));
         return;
     } else $categoryInfo = $loadCategoryInfo();
     break;
diff --git a/public-legacy/manage/news/post.php b/public-legacy/manage/news/post.php
index 66bc809b..13479f71 100644
--- a/public-legacy/manage/news/post.php
+++ b/public-legacy/manage/news/post.php
@@ -3,14 +3,11 @@ namespace Misuzu;
 
 use RuntimeException;
 
-$authInfo = $msz->getAuthInfo();
-if(!$authInfo->getPerms('global')->check(Perm::G_NEWS_POSTS_MANAGE))
+if(!$msz->authInfo->getPerms('global')->check(Perm::G_NEWS_POSTS_MANAGE))
     Template::throwError(403);
 
-$urls = $msz->getUrls();
-$news = $msz->getNews();
 $postId = (string)filter_input(INPUT_GET, 'p', FILTER_SANITIZE_NUMBER_INT);
-$loadPostInfo = fn() => $news->getPost($postId);
+$loadPostInfo = fn() => $msz->news->getPost($postId);
 
 if(empty($postId))
     $isNew = true;
@@ -26,9 +23,9 @@ if($_SERVER['REQUEST_METHOD'] === 'GET' && !empty($_GET['delete'])) {
     if(!CSRF::validateRequest())
         Template::throwError(403);
 
-    $news->deletePost($postInfo);
-    $msz->createAuditLog('NEWS_POST_DELETE', [$postInfo->getId()]);
-    Tools::redirect($urls->format('manage-news-posts'));
+    $msz->news->deletePost($postInfo);
+    $msz->createAuditLog('NEWS_POST_DELETE', [$postInfo->id]);
+    Tools::redirect($msz->urls->format('manage-news-posts'));
     return;
 }
 
@@ -39,40 +36,40 @@ while($_SERVER['REQUEST_METHOD'] === 'POST' && CSRF::validateRequest()) {
     $body = trim((string)filter_input(INPUT_POST, 'np_body'));
 
     if($isNew) {
-        $postInfo = $news->createPost($category, $title, $body, $featured, $authInfo->getUserInfo());
+        $postInfo = $msz->news->createPost($category, $title, $body, $featured, $msz->authInfo->userInfo);
     } else {
-        if($category === $postInfo->getCategoryId())
+        if($category === $postInfo->categoryId)
             $category = null;
-        if($title === $postInfo->getTitle())
+        if($title === $postInfo->title)
             $title = null;
-        if($body === $postInfo->getBody())
+        if($body === $postInfo->body)
             $body = null;
-        if($featured === $postInfo->isFeatured())
+        if($featured === $postInfo->featured)
             $featured = null;
 
         if($category !== null || $title !== null || $body !== null || $featured !== null)
-            $news->updatePost($postInfo, $category, $title, $body, $featured);
+            $msz->news->updatePost($postInfo, $category, $title, $body, $featured);
     }
 
     $msz->createAuditLog(
         $isNew ? 'NEWS_POST_CREATE' : 'NEWS_POST_EDIT',
-        [$postInfo->getId()]
+        [$postInfo->id]
     );
 
     if($isNew) {
-        if($postInfo->isFeatured()) {
+        if($postInfo->featured) {
             // Twitter integration used to be here, replace with Railgun Pulse integration
         }
 
-        Tools::redirect($urls->format('manage-news-post', ['post' => $postInfo->getId()]));
+        Tools::redirect($msz->urls->format('manage-news-post', ['post' => $postInfo->id]));
         return;
     } else $postInfo = $loadPostInfo();
     break;
 }
 
 $categories = [];
-foreach($news->getCategories() as $categoryInfo)
-    $categories[$categoryInfo->getId()] = $categoryInfo->getName();
+foreach($msz->news->getCategories() as $categoryInfo)
+    $categories[$categoryInfo->id] = $categoryInfo->name;
 
 Template::render('manage.news.post', [
     'categories' => $categories,
diff --git a/public-legacy/manage/news/posts.php b/public-legacy/manage/news/posts.php
index 93643fd0..46dd1c40 100644
--- a/public-legacy/manage/news/posts.php
+++ b/public-legacy/manage/news/posts.php
@@ -1,11 +1,10 @@
 <?php
 namespace Misuzu;
 
-if(!$msz->getAuthInfo()->getPerms('global')->check(Perm::G_NEWS_POSTS_MANAGE))
+if(!$msz->authInfo->getPerms('global')->check(Perm::G_NEWS_POSTS_MANAGE))
     Template::throwError(403);
 
-$news = $msz->getNews();
-$pagination = new Pagination($news->countPosts(
+$pagination = new Pagination($msz->news->countPosts(
     includeScheduled: true,
     includeDeleted: true
 ), 15);
@@ -13,7 +12,7 @@ $pagination = new Pagination($news->countPosts(
 if(!$pagination->hasValidOffset())
     Template::throwError(404);
 
-$posts = $news->getPosts(
+$posts = $msz->news->getPosts(
     includeScheduled: true,
     includeDeleted: true,
     pagination: $pagination
diff --git a/public-legacy/manage/users/ban.php b/public-legacy/manage/users/ban.php
index 0680f712..e2d26ace 100644
--- a/public-legacy/manage/users/ban.php
+++ b/public-legacy/manage/users/ban.php
@@ -5,37 +5,32 @@ use DateTimeInterface;
 use RuntimeException;
 use Carbon\CarbonImmutable;
 
-$authInfo = $msz->getAuthInfo();
-if(!$authInfo->getPerms('user')->check(Perm::U_BANS_MANAGE))
+if(!$msz->authInfo->getPerms('user')->check(Perm::U_BANS_MANAGE))
     Template::throwError(403);
 
-$urls = $msz->getUrls();
-$usersCtx = $msz->getUsersContext();
-$bans = $usersCtx->getBans();
-
 if($_SERVER['REQUEST_METHOD'] === 'GET' && filter_has_var(INPUT_GET, 'delete')) {
     if(!CSRF::validateRequest())
         Template::throwError(403);
 
     try {
-        $banInfo = $bans->getBan((string)filter_input(INPUT_GET, 'b'));
+        $banInfo = $msz->usersCtx->bans->getBan((string)filter_input(INPUT_GET, 'b'));
     } catch(RuntimeException $ex) {
         Template::throwError(404);
     }
 
-    $bans->deleteBans($banInfo);
-    $msz->createAuditLog('BAN_DELETE', [$banInfo->getId(), $banInfo->getUserId()]);
-    Tools::redirect($urls->format('manage-users-bans', ['user' => $banInfo->getUserId()]));
+    $msz->usersCtx->bans->deleteBans($banInfo);
+    $msz->createAuditLog('BAN_DELETE', [$banInfo->id, $banInfo->userId]);
+    Tools::redirect($msz->urls->format('manage-users-bans', ['user' => $banInfo->userId]));
     return;
 }
 
 try {
-    $userInfo = $usersCtx->getUserInfo(filter_input(INPUT_GET, 'u', FILTER_SANITIZE_NUMBER_INT), 'id');
+    $userInfo = $msz->usersCtx->getUserInfo(filter_input(INPUT_GET, 'u', FILTER_SANITIZE_NUMBER_INT), 'id');
 } catch(RuntimeException $ex) {
     Template::throwError(404);
 }
 
-$modInfo = $authInfo->getUserInfo();
+$modInfo = $msz->authInfo->userInfo;
 
 while($_SERVER['REQUEST_METHOD'] === 'POST' && CSRF::validateRequest()) {
     $expires = (int)filter_input(INPUT_POST, 'ub_expires', FILTER_SANITIZE_NUMBER_INT);
@@ -64,13 +59,13 @@ while($_SERVER['REQUEST_METHOD'] === 'POST' && CSRF::validateRequest()) {
     } else
         $expires = time() + $expires;
 
-    $banInfo = $bans->createBan(
+    $banInfo = $msz->usersCtx->bans->createBan(
         $userInfo, $expires, $publicReason, $privateReason,
         severity: $severity, modInfo: $modInfo
     );
 
-    $msz->createAuditLog('BAN_CREATE', [$banInfo->getId(), $userInfo->getId()]);
-    Tools::redirect($urls->format('manage-users-bans', ['user' => $userInfo->getId()]));
+    $msz->createAuditLog('BAN_CREATE', [$banInfo->id, $userInfo->getId()]);
+    Tools::redirect($msz->urls->format('manage-users-bans', ['user' => $userInfo->getId()]));
     return;
 }
 
diff --git a/public-legacy/manage/users/bans.php b/public-legacy/manage/users/bans.php
index f8640863..f4afadd5 100644
--- a/public-legacy/manage/users/bans.php
+++ b/public-legacy/manage/users/bans.php
@@ -3,40 +3,37 @@ namespace Misuzu;
 
 use RuntimeException;
 
-if(!$msz->getAuthInfo()->getPerms('user')->check(Perm::U_BANS_MANAGE))
+if(!$msz->authInfo->getPerms('user')->check(Perm::U_BANS_MANAGE))
     Template::throwError(403);
 
-$usersCtx = $msz->getUsersContext();
-$bans = $usersCtx->getBans();
-
 $filterUser = null;
 if(filter_has_var(INPUT_GET, 'u')) {
     $filterUserId = filter_input(INPUT_GET, 'u', FILTER_SANITIZE_NUMBER_INT);
     try {
-        $filterUser = $usersCtx->getUserInfo($filterUserId);
+        $filterUser = $msz->usersCtx->getUserInfo($filterUserId);
     } catch(RuntimeException $ex) {
         Template::throwError(404);
     }
 }
 
-$pagination = new Pagination($bans->countBans(userInfo: $filterUser), 10);
+$pagination = new Pagination($msz->usersCtx->bans->countBans(userInfo: $filterUser), 10);
 
 if(!$pagination->hasValidOffset())
     Template::throwError(404);
 
 $banList = [];
-$banInfos = $bans->getBans(userInfo: $filterUser, activeFirst: true, pagination: $pagination);
+$banInfos = $msz->usersCtx->bans->getBans(userInfo: $filterUser, activeFirst: true, pagination: $pagination);
 
 foreach($banInfos as $banInfo) {
-    $userInfo = $usersCtx->getUserInfo($banInfo->getUserId());
-    $userColour = $usersCtx->getUserColour($userInfo);
+    $userInfo = $msz->usersCtx->getUserInfo($banInfo->userId);
+    $userColour = $msz->usersCtx->getUserColour($userInfo);
 
-    if(!$banInfo->hasModId()) {
+    if($banInfo->modId === null) {
         $modInfo = null;
         $modColour = null;
     } else {
-        $modInfo = $usersCtx->getUserInfo($banInfo->getModId());
-        $modColour = $usersCtx->getUserColour($modInfo);
+        $modInfo = $msz->usersCtx->getUserInfo($banInfo->modId);
+        $modColour = $msz->usersCtx->getUserColour($modInfo);
     }
 
     $banList[] = [
diff --git a/public-legacy/manage/users/index.php b/public-legacy/manage/users/index.php
index b7918486..1962b035 100644
--- a/public-legacy/manage/users/index.php
+++ b/public-legacy/manage/users/index.php
@@ -1,29 +1,28 @@
 <?php
 namespace Misuzu;
 
-if(!$msz->getAuthInfo()->getPerms('user')->check(Perm::U_USERS_MANAGE))
+use Misuzu\Users\Roles;
+
+if(!$msz->authInfo->getPerms('user')->check(Perm::U_USERS_MANAGE))
     Template::throwError(403);
 
-$usersCtx = $msz->getUsersContext();
-$users = $usersCtx->getUsers();
-$roles = $usersCtx->getRoles();
-$pagination = new Pagination($users->countUsers(), 30);
+$pagination = new Pagination($msz->usersCtx->users->countUsers(), 30);
 
 if(!$pagination->hasValidOffset())
     Template::throwError(404);
 
 $userList = [];
-$userInfos = $users->getUsers(pagination: $pagination, orderBy: 'id');
+$userInfos = $msz->usersCtx->users->getUsers(pagination: $pagination, orderBy: 'id');
 $roleInfos = [];
 
 foreach($userInfos as $userInfo) {
-    $displayRoleId = $userInfo->getDisplayRoleId() ?? '1';
+    $displayRoleId = $userInfo->displayRoleId ?? Roles::DEFAULT_ROLE;
     if(array_key_exists($displayRoleId, $roleInfos))
         $roleInfo = $roleInfos[$displayRoleId];
     else
-        $roleInfos[$displayRoleId] = $roleInfo = $roles->getRole($displayRoleId);
+        $roleInfos[$displayRoleId] = $roleInfo = $msz->usersCtx->roles->getRole($displayRoleId);
 
-    $colour = $userInfo->hasColour() ? $userInfo->getColour() : $roleInfo->getColour();
+    $colour = $userInfo->hasColour ? $userInfo->colour : $roleInfo->colour;
 
     $userList[] = [
         'info' => $userInfo,
diff --git a/public-legacy/manage/users/note.php b/public-legacy/manage/users/note.php
index e49d2c69..aff56313 100644
--- a/public-legacy/manage/users/note.php
+++ b/public-legacy/manage/users/note.php
@@ -3,8 +3,7 @@ namespace Misuzu;
 
 use RuntimeException;
 
-$authInfo = $msz->getAuthInfo();
-if(!$authInfo->getPerms('user')->check(Perm::U_NOTES_MANAGE))
+if(!$msz->authInfo->getPerms('user')->check(Perm::U_NOTES_MANAGE))
     Template::throwError(403);
 
 $hasNoteId = filter_has_var(INPUT_GET, 'n');
@@ -13,25 +12,21 @@ $hasUserId = filter_has_var(INPUT_GET, 'u');
 if((!$hasNoteId && !$hasUserId) || ($hasNoteId && $hasUserId))
     Template::throwError(400);
 
-$urls = $msz->getUrls();
-$usersCtx = $msz->getUsersContext();
-$modNotes = $usersCtx->getModNotes();
-
 if($hasUserId) {
     $isNew = true;
 
     try {
-        $userInfo = $usersCtx->getUserInfo(filter_input(INPUT_GET, 'u', FILTER_SANITIZE_NUMBER_INT));
+        $userInfo = $msz->usersCtx->getUserInfo(filter_input(INPUT_GET, 'u', FILTER_SANITIZE_NUMBER_INT));
     } catch(RuntimeException $ex) {
         Template::throwError(404);
     }
 
-    $authorInfo = $authInfo->getUserInfo();
+    $authorInfo = $msz->authInfo->userInfo;
 } elseif($hasNoteId) {
     $isNew = false;
 
     try {
-        $noteInfo = $modNotes->getNote((string)filter_input(INPUT_GET, 'n', FILTER_SANITIZE_NUMBER_INT));
+        $noteInfo = $msz->usersCtx->modNotes->getNote((string)filter_input(INPUT_GET, 'n', FILTER_SANITIZE_NUMBER_INT));
     } catch(RuntimeException $ex) {
         Template::throwError(404);
     }
@@ -40,14 +35,14 @@ if($hasUserId) {
         if(!CSRF::validateRequest())
             Template::throwError(403);
 
-        $modNotes->deleteNotes($noteInfo);
-        $msz->createAuditLog('MOD_NOTE_DELETE', [$noteInfo->getId(), $noteInfo->getUserId()]);
-        Tools::redirect($urls->format('manage-users-notes', ['user' => $noteInfo->getUserId()]));
+        $msz->usersCtx->modNotes->deleteNotes($noteInfo);
+        $msz->createAuditLog('MOD_NOTE_DELETE', [$noteInfo->id, $noteInfo->userId]);
+        Tools::redirect($msz->urls->format('manage-users-notes', ['user' => $noteInfo->userId]));
         return;
     }
 
-    $userInfo = $usersCtx->getUserInfo($noteInfo->getUserId());
-    $authorInfo = $noteInfo->hasAuthorId() ? $usersCtx->getUserInfo($noteInfo->getAuthorId()) : null;
+    $userInfo = $msz->usersCtx->getUserInfo($noteInfo->userId);
+    $authorInfo = $noteInfo->authorId !== null ? $msz->usersCtx->getUserInfo($noteInfo->authorId) : null;
 }
 
 while($_SERVER['REQUEST_METHOD'] === 'POST' && CSRF::validateRequest()) {
@@ -55,24 +50,24 @@ while($_SERVER['REQUEST_METHOD'] === 'POST' && CSRF::validateRequest()) {
     $body = trim((string)filter_input(INPUT_POST, 'mn_body'));
 
     if($isNew) {
-        $noteInfo = $modNotes->createNote($userInfo, $title, $body, $authorInfo);
+        $noteInfo = $msz->usersCtx->modNotes->createNote($userInfo, $title, $body, $authorInfo);
     } else {
-        if($title === $noteInfo->getTitle())
+        if($title === $noteInfo->title)
             $title = null;
-        if($body === $noteInfo->getBody())
+        if($body === $noteInfo->body)
             $body = null;
 
         if($title !== null || $body !== null)
-            $modNotes->updateNote($noteInfo, $title, $body);
+            $msz->usersCtx->modNotes->updateNote($noteInfo, $title, $body);
     }
 
     $msz->createAuditLog(
         $isNew ? 'MOD_NOTE_CREATE' : 'MOD_NOTE_UPDATE',
-        [$noteInfo->getId(), $userInfo->getId()]
+        [$noteInfo->id, $userInfo->getId()]
     );
 
     // this is easier
-    Tools::redirect($urls->format('manage-users-note', ['note' => $noteInfo->getId()]));
+    Tools::redirect($msz->urls->format('manage-users-note', ['note' => $noteInfo->id]));
     return;
 }
 
@@ -80,7 +75,7 @@ Template::render('manage.users.note', [
     'note_new' => $isNew,
     'note_info' => $noteInfo ?? null,
     'note_user' => $userInfo,
-    'note_user_colour' => $usersCtx->getUserColour($userInfo),
+    'note_user_colour' => $msz->usersCtx->getUserColour($userInfo),
     'note_author' => $authorInfo,
-    'note_author_colour' => $usersCtx->getUserColour($authorInfo),
+    'note_author_colour' => $msz->usersCtx->getUserColour($authorInfo),
 ]);
diff --git a/public-legacy/manage/users/notes.php b/public-legacy/manage/users/notes.php
index 618a3453..ef833d88 100644
--- a/public-legacy/manage/users/notes.php
+++ b/public-legacy/manage/users/notes.php
@@ -3,40 +3,37 @@ namespace Misuzu;
 
 use RuntimeException;
 
-if(!$msz->getAuthInfo()->getPerms('user')->check(Perm::U_NOTES_MANAGE))
+if(!$msz->authInfo->getPerms('user')->check(Perm::U_NOTES_MANAGE))
     Template::throwError(403);
 
-$usersCtx = $msz->getUsersContext();
-$modNotes = $usersCtx->getModNotes();
-
 $filterUser = null;
 if(filter_has_var(INPUT_GET, 'u')) {
     $filterUserId = filter_input(INPUT_GET, 'u', FILTER_SANITIZE_NUMBER_INT);
     try {
-        $filterUser = $usersCtx->getUserInfo($filterUserId);
+        $filterUser = $msz->usersCtx->getUserInfo($filterUserId);
     } catch(RuntimeException $ex) {
         Template::throwError(404);
     }
 }
 
-$pagination = new Pagination($modNotes->countNotes(userInfo: $filterUser), 10);
+$pagination = new Pagination($msz->usersCtx->modNotes->countNotes(userInfo: $filterUser), 10);
 
 if(!$pagination->hasValidOffset())
     Template::throwError(404);
 
 $notes = [];
-$noteInfos = $modNotes->getNotes(userInfo: $filterUser, pagination: $pagination);
+$noteInfos = $msz->usersCtx->modNotes->getNotes(userInfo: $filterUser, pagination: $pagination);
 
 foreach($noteInfos as $noteInfo) {
-    $userInfo = $usersCtx->getUserInfo($noteInfo->getUserId());
-    $userColour = $usersCtx->getUserColour($userInfo);
+    $userInfo = $msz->usersCtx->getUserInfo($noteInfo->userId);
+    $userColour = $msz->usersCtx->getUserColour($userInfo);
 
-    if(!$noteInfo->hasAuthorId()) {
+    if($noteInfo->authorId === null) {
         $authorInfo = null;
         $authorColour = null;
     } else {
-        $authorInfo = $usersCtx->getUserInfo($noteInfo->getAuthorId());
-        $authorColour = $usersCtx->getUserColour($authorInfo);
+        $authorInfo = $msz->usersCtx->getUserInfo($noteInfo->authorId);
+        $authorColour = $msz->usersCtx->getUserColour($authorInfo);
     }
 
     $notes[] = [
diff --git a/public-legacy/manage/users/role.php b/public-legacy/manage/users/role.php
index ab94b14b..e9917f3e 100644
--- a/public-legacy/manage/users/role.php
+++ b/public-legacy/manage/users/role.php
@@ -6,38 +6,33 @@ use Index\Colour\Colour;
 use Index\Colour\ColourRgb;
 use Misuzu\Perm;
 
-$authInfo = $msz->getAuthInfo();
-$viewerPerms = $authInfo->getPerms('user');
+$viewerPerms = $msz->authInfo->getPerms('user');
 if(!$viewerPerms->check(Perm::U_ROLES_MANAGE))
     Template::throwError(403);
 
 $roleInfo = null;
-$usersCtx = $msz->getUsersContext();
-$users = $usersCtx->getUsers();
-$roles = $usersCtx->getRoles();
-$perms = $msz->getPerms();
 
 if(filter_has_var(INPUT_GET, 'r')) {
     $roleId = (string)filter_input(INPUT_GET, 'r', FILTER_SANITIZE_NUMBER_INT);
 
     try {
         $isNew = false;
-        $roleInfo = $roles->getRole($roleId);
+        $roleInfo = $msz->usersCtx->roles->getRole($roleId);
     } catch(RuntimeException $ex) {
         Template::throwError(404);
     }
 } else $isNew = true;
 
-$currentUser = $authInfo->getUserInfo();
+$currentUser = $msz->authInfo->userInfo;
 $canEditPerms = $viewerPerms->check(Perm::U_PERMS_MANAGE);
 
-$permsInfos = $roleInfo === null ? null : $perms->getPermissionInfo(roleInfo: $roleInfo, categoryNames: Perm::INFO_FOR_ROLE);
+$permsInfos = $roleInfo === null ? null : $msz->perms->getPermissionInfo(roleInfo: $roleInfo, categoryNames: Perm::INFO_FOR_ROLE);
 $permsLists = Perm::createList(Perm::LISTS_FOR_ROLE);
 
 while($_SERVER['REQUEST_METHOD'] === 'POST' && CSRF::validateRequest()) {
-    $userRank = $users->getUserRank($currentUser);
+    $userRank = $msz->usersCtx->users->getUserRank($currentUser);
 
-    if(!$isNew && !$currentUser->isSuperUser() && $roleInfo->getRank() >= $userRank) {
+    if(!$isNew && !$currentUser->super && $roleInfo->rank >= $userRank) {
         echo 'You aren\'t allowed to edit this role.';
         break;
     }
@@ -68,7 +63,7 @@ while($_SERVER['REQUEST_METHOD'] === 'POST' && CSRF::validateRequest()) {
         'role_ur_col_blue' => $colourBlue,
     ]);
 
-    if(!$currentUser->isSuperUser() && $roleRank >= $userRank) {
+    if(!$currentUser->super && $roleRank >= $userRank) {
         echo 'You aren\'t allowed to make a role with equal rank to your own.';
         break;
     }
@@ -108,7 +103,7 @@ while($_SERVER['REQUEST_METHOD'] === 'POST' && CSRF::validateRequest()) {
     }
 
     if($isNew) {
-        $roleInfo = $roles->createRole(
+        $roleInfo = $msz->usersCtx->roles->createRole(
             $roleName,
             $roleRank,
             $roleColour,
@@ -119,25 +114,25 @@ while($_SERVER['REQUEST_METHOD'] === 'POST' && CSRF::validateRequest()) {
             leavable: $roleLeavable
         );
     } else {
-        if($roleName === $roleInfo->getName())
+        if($roleName === $roleInfo->name)
             $roleName = null;
-        if($roleString === $roleInfo->getString())
+        if($roleString === $roleInfo->string)
             $roleString = null;
-        if($roleHide === $roleInfo->isHidden())
+        if($roleHide === $roleInfo->hidden)
             $roleHide = null;
-        if($roleLeavable === $roleInfo->isLeavable())
+        if($roleLeavable === $roleInfo->leavable)
             $roleLeavable = null;
-        if($roleRank === $roleInfo->getRank())
+        if($roleRank === $roleInfo->rank)
             $roleRank = null;
-        if($roleTitle === $roleInfo->getTitle())
+        if($roleTitle === $roleInfo->title)
             $roleTitle = null;
-        if($roleDesc === $roleInfo->getDescription())
+        if($roleDesc === $roleInfo->description)
             $roleDesc = null;
         // local genius did not implement colour comparison
-        if((string)$roleColour === (string)$roleInfo->getColour())
+        if((string)$roleColour === (string)$roleInfo->colour)
             $roleColour = null;
 
-        $roles->updateRole(
+        $msz->usersCtx->roles->updateRole(
             $roleInfo,
             string: $roleString,
             name: $roleName,
@@ -152,7 +147,7 @@ while($_SERVER['REQUEST_METHOD'] === 'POST' && CSRF::validateRequest()) {
 
     $msz->createAuditLog(
         $isNew ? 'ROLE_CREATE' : 'ROLE_UPDATE',
-        [$roleInfo->getId()]
+        [$roleInfo->id]
     );
 
     if($canEditPerms && filter_has_var(INPUT_POST, 'perms')) {
@@ -162,13 +157,13 @@ while($_SERVER['REQUEST_METHOD'] === 'POST' && CSRF::validateRequest()) {
         );
 
         foreach($permsApply as $categoryName => $values)
-            $perms->setPermissions($categoryName, $values['allow'], $values['deny'], roleInfo: $roleInfo);
+            $msz->perms->setPermissions($categoryName, $values['allow'], $values['deny'], roleInfo: $roleInfo);
 
         // could target all users with the role but ech
-        $msz->getConfig()->setBoolean('perms.needsRecalc', true);
+        $msz->config->setBoolean('perms.needsRecalc', true);
     }
 
-    Tools::redirect($msz->getUrls()->format('manage-role', ['role' => $roleInfo->getId()]));
+    Tools::redirect($msz->urls->format('manage-role', ['role' => $roleInfo->id]));
     return;
 }
 
diff --git a/public-legacy/manage/users/roles.php b/public-legacy/manage/users/roles.php
index 2efc7147..6d7f94cf 100644
--- a/public-legacy/manage/users/roles.php
+++ b/public-legacy/manage/users/roles.php
@@ -1,10 +1,10 @@
 <?php
 namespace Misuzu;
 
-if(!$msz->getAuthInfo()->getPerms('user')->check(Perm::U_ROLES_MANAGE))
+if(!$msz->authInfo->getPerms('user')->check(Perm::U_ROLES_MANAGE))
     Template::throwError(403);
 
-$roles = $msz->getUsersContext()->getRoles();
+$roles = $msz->usersCtx->roles;
 $pagination = new Pagination($roles->countRoles(), 10);
 
 if(!$pagination->hasValidOffset())
diff --git a/public-legacy/manage/users/user.php b/public-legacy/manage/users/user.php
index 40f18d96..45b8c4f2 100644
--- a/public-legacy/manage/users/user.php
+++ b/public-legacy/manage/users/user.php
@@ -7,18 +7,11 @@ use Misuzu\Perm;
 use Misuzu\Auth\AuthTokenCookie;
 use Misuzu\Users\User;
 
-$authInfo = $msz->getAuthInfo();
-$viewerPerms = $authInfo->getPerms('user');
-if(!$authInfo->isLoggedIn())
+$viewerPerms = $msz->authInfo->getPerms('user');
+if(!$msz->authInfo->isLoggedIn)
     Template::throwError(403);
 
-$urls = $msz->getUrls();
-$usersCtx = $msz->getUsersContext();
-$users = $usersCtx->getUsers();
-$roles = $usersCtx->getRoles();
-$perms = $msz->getPerms();
-
-$currentUser = $authInfo->getUserInfo();
+$currentUser = $msz->authInfo->userInfo;
 
 $canManageUsers = $viewerPerms->check(Perm::U_USERS_MANAGE);
 $canManagePerms = $viewerPerms->check(Perm::U_PERMS_MANAGE);
@@ -26,7 +19,7 @@ $canManageNotes = $viewerPerms->check(Perm::U_NOTES_MANAGE);
 $canManageWarnings = $viewerPerms->check(Perm::U_WARNINGS_MANAGE);
 $canManageBans = $viewerPerms->check(Perm::U_BANS_MANAGE);
 $canImpersonate = $viewerPerms->check(Perm::U_CAN_IMPERSONATE);
-$canSendTestMail = $currentUser->isSuperUser();
+$canSendTestMail = $currentUser->super;
 $hasAccess = $canManageUsers || $canManageNotes || $canManageWarnings || $canManageBans;
 
 if(!$hasAccess)
@@ -36,18 +29,18 @@ $notices = [];
 $userId = (int)filter_input(INPUT_GET, 'u', FILTER_SANITIZE_NUMBER_INT);
 
 try {
-    $userInfo = $users->getUser($userId, 'id');
+    $userInfo = $msz->usersCtx->users->getUser($userId, 'id');
 } catch(RuntimeException $ex) {
     Template::throwError(404);
 }
 
-$currentUserRank = $users->getUserRank($currentUser);
-$userRank = $users->getUserRank($userInfo);
+$currentUserRank = $msz->usersCtx->users->getUserRank($currentUser);
+$userRank = $msz->usersCtx->users->getUserRank($userInfo);
 
-$canEdit = $canManageUsers && ($currentUser->isSuperUser() || (string)$currentUser->getId() === $userInfo->getId() || $currentUserRank > $userRank);
+$canEdit = $canManageUsers && ($currentUser->super || (string)$currentUser->getId() === $userInfo->getId() || $currentUserRank > $userRank);
 $canEditPerms = $canEdit && $canManagePerms;
 
-$permsInfos = $perms->getPermissionInfo(userInfo: $userInfo, categoryNames: Perm::INFO_FOR_USER);
+$permsInfos = $msz->perms->getPermissionInfo(userInfo: $userInfo, categoryNames: Perm::INFO_FOR_USER);
 $permsLists = Perm::createList(Perm::LISTS_FOR_USER);
 $permsNeedRecalc = false;
 
@@ -58,22 +51,22 @@ if(CSRF::validateRequest() && $canEdit) {
         } elseif(!is_string($_POST['impersonate_user']) || $_POST['impersonate_user'] !== 'meow') {
             $notices[] = 'You didn\'t say the magic word.';
         } else {
-            $allowToImpersonate = $currentUser->isSuperUser();
+            $allowToImpersonate = $currentUser->super;
 
             if(!$allowToImpersonate) {
-                $allowImpersonateUsers = $msz->getConfig()->getArray(sprintf('impersonate.allow.u%s', $currentUser->getId()));
+                $allowImpersonateUsers = $msz->config->getArray(sprintf('impersonate.allow.u%s', $currentUser->getId()));
                 $allowToImpersonate = in_array($userInfo->getId(), $allowImpersonateUsers, true);
             }
 
             if($allowToImpersonate) {
-                $msz->createAuditLog('USER_IMPERSONATE', [$userInfo->getId(), $userInfo->getName()]);
+                $msz->createAuditLog('USER_IMPERSONATE', [$userInfo->getId(), $userInfo->name]);
 
-                $tokenBuilder = $authInfo->getTokenInfo()->toBuilder();
+                $tokenBuilder = $msz->authInfo->tokenInfo->toBuilder();
                 $tokenBuilder->setImpersonatedUserId($userInfo->getId());
                 $tokenInfo = $tokenBuilder->toInfo();
 
                 AuthTokenCookie::apply($tokenPacker->pack($tokenInfo));
-                Tools::redirect($urls->format('index'));
+                Tools::redirect($msz->urls->format('index'));
                 return;
             } else $notices[] = 'You aren\'t allowed to impersonate this user.';
         }
@@ -86,7 +79,7 @@ if(CSRF::validateRequest() && $canEdit) {
             $notices[] = 'Invalid request thing shut the fuck up.';
         } else {
             $testMail = Mailer::sendMessage(
-                [$userInfo->getEMailAddress() => $userInfo->getName()],
+                [$userInfo->emailAddress => $userInfo->name],
                 'Flashii Test E-mail',
                 'You were sent this e-mail to validate if you can receive e-mails from Flashii. You may discard it.'
             );
@@ -106,13 +99,13 @@ if(CSRF::validateRequest() && $canEdit) {
         }
 
         $existingRoles = [];
-        foreach(iterator_to_array($roles->getRoles(userInfo: $userInfo)) as $roleInfo)
-            $existingRoles[$roleInfo->getId()] = $roleInfo;
+        foreach(iterator_to_array($msz->usersCtx->roles->getRoles(userInfo: $userInfo)) as $roleInfo)
+            $existingRoles[$roleInfo->id] = $roleInfo;
 
         $removeRoles = [];
 
         foreach($existingRoles as $roleInfo) {
-            if($roleInfo->isDefault() || !($currentUser->isSuperUser() || $userRank > $roleInfo->getRank()))
+            if($roleInfo->default || !($currentUser->super || $userRank > $roleInfo->rank))
                 continue;
 
             if(!in_array($roleInfo->getId(), $applyRoles))
@@ -120,18 +113,18 @@ if(CSRF::validateRequest() && $canEdit) {
         }
 
         if(!empty($removeRoles))
-            $users->removeRoles($userInfo, $removeRoles);
+            $msz->usersCtx->users->removeRoles($userInfo, $removeRoles);
 
         $addRoles = [];
 
         foreach($applyRoles as $roleId) {
             try {
-                $roleInfo = $existingRoles[$roleId] ?? $roles->getRole($roleId);
+                $roleInfo = $existingRoles[$roleId] ?? $msz->usersCtx->roles->getRole($roleId);
             } catch(RuntimeException $ex) {
                 continue;
             }
 
-            if(!$currentUser->isSuperUser() && $userRank <= $roleInfo->getRank())
+            if(!$currentUser->super && $userRank <= $roleInfo->rank)
                 continue;
 
             if(!in_array($roleInfo, $existingRoles))
@@ -139,7 +132,7 @@ if(CSRF::validateRequest() && $canEdit) {
         }
 
         if(!empty($addRoles))
-            $users->addRoles($userInfo, $addRoles);
+            $msz->usersCtx->users->addRoles($userInfo, $addRoles);
 
         if(!empty($addRoles) || !empty($removeRoles))
             $permsNeedRecalc = true;
@@ -150,7 +143,7 @@ if(CSRF::validateRequest() && $canEdit) {
         $setTitle = (string)($_POST['user']['title'] ?? '');
 
         $displayRole = (string)($_POST['user']['display_role'] ?? 0);
-        if(!$users->hasRole($userInfo, $displayRole))
+        if(!$msz->usersCtx->users->hasRole($userInfo, $displayRole))
             $notices[] = 'User does not have the role you\'re trying to assign as primary.';
 
         $countryValidation = strlen($setCountry) === 2
@@ -164,7 +157,7 @@ if(CSRF::validateRequest() && $canEdit) {
             $notices[] = 'User title was invalid.';
 
         if(empty($notices)) {
-            $users->updateUser(
+            $msz->usersCtx->users->updateUser(
                 userInfo: $userInfo,
                 displayRoleInfo: $displayRole,
                 countryCode: (string)($_POST['user']['country'] ?? 'XX'),
@@ -183,7 +176,7 @@ if(CSRF::validateRequest() && $canEdit) {
         }
 
         if(empty($notices))
-            $users->updateUser(userInfo: $userInfo, colour: $setColour);
+            $msz->usersCtx->users->updateUser(userInfo: $userInfo, colour: $setColour);
     }
 
     if(!empty($_POST['password']) && is_array($_POST['password'])) {
@@ -194,13 +187,13 @@ if(CSRF::validateRequest() && $canEdit) {
             if($passwordNewValue !== $passwordConfirmValue)
                 $notices[] = 'Confirm password does not match.';
             else {
-                $passwordValidation = $users->validatePassword($passwordNewValue);
+                $passwordValidation = $msz->usersCtx->users->validatePassword($passwordNewValue);
                 if($passwordValidation !== '')
-                   $notices[] = $users->validatePasswordText($passwordValidation);
+                   $notices[] = $msz->usersCtx->users->validatePasswordText($passwordValidation);
             }
 
             if(empty($notices))
-                $users->updateUser(userInfo: $userInfo, password: $passwordNewValue);
+                $msz->usersCtx->users->updateUser(userInfo: $userInfo, password: $passwordNewValue);
         }
     }
 
@@ -211,23 +204,23 @@ if(CSRF::validateRequest() && $canEdit) {
         );
 
         foreach($permsApply as $categoryName => $values)
-            $perms->setPermissions($categoryName, $values['allow'], $values['deny'], userInfo: $userInfo);
+            $msz->perms->setPermissions($categoryName, $values['allow'], $values['deny'], userInfo: $userInfo);
 
         $permsNeedRecalc = true;
     }
 
     if($permsNeedRecalc)
-        $perms->precalculatePermissions(
-            $msz->getForumContext()->getCategories(),
+        $msz->perms->precalculatePermissions(
+            $msz->forumCtx->categories,
             [$userInfo->getId()]
         );
 
-    Tools::redirect($urls->format('manage-user', ['user' => $userInfo->getId()]));
+    Tools::redirect($msz->urls->format('manage-user', ['user' => $userInfo->getId()]));
     return;
 }
 
-$rolesAll = iterator_to_array($roles->getRoles());
-$userRoleIds = $users->hasRoles($userInfo, $rolesAll);
+$rolesAll = iterator_to_array($msz->usersCtx->roles->getRoles());
+$userRoleIds = $msz->usersCtx->users->hasRoles($userInfo, $rolesAll);
 
 Template::render('manage.users.user', [
     'user_info' => $userInfo,
diff --git a/public-legacy/manage/users/warning.php b/public-legacy/manage/users/warning.php
index 68f35067..5196fcd9 100644
--- a/public-legacy/manage/users/warning.php
+++ b/public-legacy/manage/users/warning.php
@@ -3,49 +3,43 @@ namespace Misuzu;
 
 use RuntimeException;
 
-$authInfo = $msz->getAuthInfo();
-if(!$authInfo->getPerms('user')->check(Perm::U_WARNINGS_MANAGE))
+if(!$msz->authInfo->getPerms('user')->check(Perm::U_WARNINGS_MANAGE))
     Template::throwError(403);
 
-$urls = $msz->getUrls();
-$usersCtx = $msz->getUsersContext();
-$users = $usersCtx->getUsers();
-$warns = $usersCtx->getWarnings();
-
 if($_SERVER['REQUEST_METHOD'] === 'GET' && filter_has_var(INPUT_GET, 'delete')) {
     if(!CSRF::validateRequest())
         Template::throwError(403);
 
     try {
-        $warnInfo = $warns->getWarning((string)filter_input(INPUT_GET, 'w'));
+        $warnInfo = $msz->usersCtx->warnings->getWarning((string)filter_input(INPUT_GET, 'w'));
     } catch(RuntimeException $ex) {
         Template::throwError(404);
     }
 
-    $warns->deleteWarnings($warnInfo);
-    $msz->createAuditLog('WARN_DELETE', [$warnInfo->getId(), $warnInfo->getUserId()]);
-    Tools::redirect($urls->format('manage-users-warnings', ['user' => $warnInfo->getUserId()]));
+    $msz->usersCtx->warnings->deleteWarnings($warnInfo);
+    $msz->createAuditLog('WARN_DELETE', [$warnInfo->id, $warnInfo->userId]);
+    Tools::redirect($msz->urls->format('manage-users-warnings', ['user' => $warnInfo->userId]));
     return;
 }
 
 try {
-    $userInfo = $users->getUser(filter_input(INPUT_GET, 'u', FILTER_SANITIZE_NUMBER_INT), 'id');
+    $userInfo = $msz->usersCtx->users->getUser(filter_input(INPUT_GET, 'u', FILTER_SANITIZE_NUMBER_INT), 'id');
 } catch(RuntimeException $ex) {
     Template::throwError(404);
 }
 
-$modInfo = $authInfo->getUserInfo();
+$modInfo = $msz->authInfo->userInfo;
 
 while($_SERVER['REQUEST_METHOD'] === 'POST' && CSRF::validateRequest()) {
     $body = trim((string)filter_input(INPUT_POST, 'uw_body'));
     Template::set('warn_value_body', $body);
 
-    $warnInfo = $warns->createWarning(
+    $warnInfo = $msz->usersCtx->warnings->createWarning(
         $userInfo, $body, modInfo: $modInfo
     );
 
-    $msz->createAuditLog('WARN_CREATE', [$warnInfo->getId(), $userInfo->getId()]);
-    Tools::redirect($urls->format('manage-users-warnings', ['user' => $userInfo->getId()]));
+    $msz->createAuditLog('WARN_CREATE', [$warnInfo->id, $userInfo->getId()]);
+    Tools::redirect($msz->urls->format('manage-users-warnings', ['user' => $userInfo->getId()]));
     return;
 }
 
diff --git a/public-legacy/manage/users/warnings.php b/public-legacy/manage/users/warnings.php
index acf8d69d..b43c351a 100644
--- a/public-legacy/manage/users/warnings.php
+++ b/public-legacy/manage/users/warnings.php
@@ -3,40 +3,37 @@ namespace Misuzu;
 
 use RuntimeException;
 
-if(!$msz->getAuthInfo()->getPerms('user')->check(Perm::U_WARNINGS_MANAGE))
+if(!$msz->authInfo->getPerms('user')->check(Perm::U_WARNINGS_MANAGE))
     Template::throwError(403);
 
-$usersCtx = $msz->getUsersContext();
-$warns = $usersCtx->getWarnings();
-
 $filterUser = null;
 if(filter_has_var(INPUT_GET, 'u')) {
     $filterUserId = filter_input(INPUT_GET, 'u', FILTER_SANITIZE_NUMBER_INT);
     try {
-        $filterUser = $usersCtx->getUserInfo($filterUserId);
+        $filterUser = $msz->usersCtx->getUserInfo($filterUserId);
     } catch(RuntimeException $ex) {
         Template::throwError(404);
     }
 }
 
-$pagination = new Pagination($warns->countWarnings(userInfo: $filterUser), 10);
+$pagination = new Pagination($msz->usersCtx->warnings->countWarnings(userInfo: $filterUser), 10);
 
 if(!$pagination->hasValidOffset())
     Template::throwError(404);
 
 $warnList = [];
-$warnInfos = $warns->getWarnings(userInfo: $filterUser, pagination: $pagination);
+$warnInfos = $msz->usersCtx->warnings->getWarnings(userInfo: $filterUser, pagination: $pagination);
 
 foreach($warnInfos as $warnInfo) {
-    $userInfo = $usersCtx->getUserInfo($warnInfo->getUserId());
-    $userColour = $usersCtx->getUserColour($userInfo);
+    $userInfo = $msz->usersCtx->getUserInfo($warnInfo->userId);
+    $userColour = $msz->usersCtx->getUserColour($userInfo);
 
-    if(!$warnInfo->hasModId()) {
+    if($warnInfo->modId === null) {
         $modInfo = null;
         $modColour = null;
     } else {
-        $modInfo = $usersCtx->getUserInfo($warnInfo->getModId());
-        $modColour = $usersCtx->getUserColour($modInfo);
+        $modInfo = $msz->usersCtx->getUserInfo($warnInfo->modId);
+        $modColour = $msz->usersCtx->getUserColour($modInfo);
     }
 
     $warnList[] = [
diff --git a/public-legacy/members.php b/public-legacy/members.php
index b01117a6..edafddd0 100644
--- a/public-legacy/members.php
+++ b/public-legacy/members.php
@@ -3,17 +3,11 @@ namespace Misuzu;
 
 use RuntimeException;
 
-$authInfo = $msz->getAuthInfo();
-if(!$authInfo->isLoggedIn())
+if(!$msz->authInfo->isLoggedIn)
     Template::throwError(403);
 
 // TODO: restore forum-topics and forum-posts orderings
 
-$forumCtx = $msz->getForumContext();
-$usersCtx = $msz->getUsersContext();
-$users = $usersCtx->getUsers();
-$roles = $usersCtx->getRoles();
-
 $roleId = filter_has_var(INPUT_GET, 'r') ? (string)filter_input(INPUT_GET, 'r') : null;
 $orderBy = strtolower((string)filter_input(INPUT_GET, 'ss'));
 $orderDir = strtolower((string)filter_input(INPUT_GET, 'sd'));
@@ -66,7 +60,7 @@ if(empty($orderDir)) {
 }
 
 if($roleId === null) {
-    $roleInfo = $roles->getDefaultRole();
+    $roleInfo = $msz->usersCtx->roles->getDefaultRole();
 } else {
     try {
         $roleInfo = $roles->getRole($roleId);
@@ -75,14 +69,14 @@ if($roleId === null) {
     }
 }
 
-$canManageUsers = $authInfo->getPerms('user')->check(Perm::U_USERS_MANAGE);
+$canManageUsers = $msz->authInfo->getPerms('user')->check(Perm::U_USERS_MANAGE);
 $deleted = $canManageUsers ? null : false;
 
-$rolesAll = $roles->getRoles(hidden: false);
-$pagination = new Pagination($users->countUsers(roleInfo: $roleInfo, deleted: $deleted), 15);
+$rolesAll = $msz->usersCtx->roles->getRoles(hidden: false);
+$pagination = new Pagination($msz->usersCtx->users->countUsers(roleInfo: $roleInfo, deleted: $deleted), 15);
 
 $userList = [];
-$userInfos = $users->getUsers(
+$userInfos = $msz->usersCtx->users->getUsers(
     roleInfo: $roleInfo,
     deleted: $deleted,
     orderBy: $orderBy,
@@ -93,9 +87,9 @@ $userInfos = $users->getUsers(
 foreach($userInfos as $userInfo)
     $userList[] = [
         'info' => $userInfo,
-        'colour' => $usersCtx->getUserColour($userInfo),
-        'ftopics' => $forumCtx->countTotalUserTopics($userInfo),
-        'fposts' => $forumCtx->countTotalUserPosts($userInfo),
+        'colour' => $msz->usersCtx->getUserColour($userInfo),
+        'ftopics' => $msz->forumCtx->countTotalUserTopics($userInfo),
+        'fposts' => $msz->forumCtx->countTotalUserPosts($userInfo),
     ];
 
 if(empty($userList))
diff --git a/public-legacy/profile.php b/public-legacy/profile.php
index 8d9a2b99..acf384af 100644
--- a/public-legacy/profile.php
+++ b/public-legacy/profile.php
@@ -14,21 +14,12 @@ $userId = !empty($_GET['u']) && is_string($_GET['u']) ? trim($_GET['u']) : 0;
 $profileMode = !empty($_GET['m']) && is_string($_GET['m']) ? (string)$_GET['m'] : '';
 $isEditing = !empty($_GET['edit']) && is_string($_GET['edit']) ? (bool)$_GET['edit'] : !empty($_POST) && is_array($_POST);
 
-$urls = $msz->getUrls();
-$usersCtx = $msz->getUsersContext();
-$users = $usersCtx->getUsers();
-$forumCtx = $msz->getForumContext();
-$forumCategories = $forumCtx->getCategories();
-$forumTopics = $forumCtx->getTopics();
-$forumPosts = $forumCtx->getPosts();
-
-$authInfo = $msz->getAuthInfo();
-$viewerInfo = $authInfo->getUserInfo();
+$viewerInfo = $msz->authInfo->userInfo;
 $viewingAsGuest = $viewerInfo === null;
 $viewerId = $viewingAsGuest ? '0' : $viewerInfo->getId();
 
 try {
-    $userInfo = $usersCtx->getUserInfo($userId, 'profile');
+    $userInfo = $msz->usersCtx->getUserInfo($userId, 'profile');
 } catch(RuntimeException $ex) {
     http_response_code(404);
     Template::render('profile.index', [
@@ -39,7 +30,7 @@ try {
     return;
 }
 
-if($userInfo->isDeleted()) {
+if($userInfo->deleted) {
     http_response_code(404);
     Template::render('profile.index', [
         'profile_is_guest' => $viewingAsGuest,
@@ -54,11 +45,11 @@ switch($profileMode) {
         Template::throwError(404);
 
     case 'forum-topics':
-        Tools::redirect($urls->format('search-query', ['query' => sprintf('type:forum:topic author:%s', $userInfo->getName()), 'section' => 'topics']));
+        Tools::redirect($msz->urls->format('search-query', ['query' => sprintf('type:forum:topic author:%s', $userInfo->name), 'section' => 'topics']));
         return;
 
     case 'forum-posts':
-        Tools::redirect($urls->format('search-query', ['query' => sprintf('type:forum:post author:%s', $userInfo->getName()), 'section' => 'posts']));
+        Tools::redirect($msz->urls->format('search-query', ['query' => sprintf('type:forum:post author:%s', $userInfo->name), 'section' => 'posts']));
         return;
 
     case '':
@@ -67,18 +58,17 @@ switch($profileMode) {
 
 $notices = [];
 
-$userRank = $usersCtx->getUserRank($userInfo);
-$viewerRank = $usersCtx->getUserRank($viewerInfo);
+$userRank = $msz->usersCtx->getUserRank($userInfo);
+$viewerRank = $msz->usersCtx->getUserRank($viewerInfo);
 
-$viewerPermsGlobal = $authInfo->getPerms('global');
-$viewerPermsUser = $authInfo->getPerms('user');
+$viewerPermsGlobal = $msz->authInfo->getPerms('global');
+$viewerPermsUser = $msz->authInfo->getPerms('user');
 
-$activeBanInfo = $usersCtx->tryGetActiveBan($userInfo);
+$activeBanInfo = $msz->usersCtx->tryGetActiveBan($userInfo);
 $isBanned = $activeBanInfo !== null;
-$profileFields = $msz->getProfileFields();
 $viewingOwnProfile = (string)$viewerId === $userInfo->getId();
 $canManageWarnings = $viewerPermsUser->check(Perm::U_WARNINGS_MANAGE);
-$canEdit = !$viewingAsGuest && ((!$isBanned && $viewingOwnProfile) || $viewerInfo->isSuperUser() || (
+$canEdit = !$viewingAsGuest && ((!$isBanned && $viewingOwnProfile) || $viewerInfo->super || (
     $viewerPermsUser->check(Perm::U_USERS_MANAGE) && ($viewingOwnProfile || $viewerRank > $userRank)
 ));
 $avatarInfo = new UserAvatarAsset($userInfo);
@@ -112,13 +102,13 @@ if($isEditing) {
                 if(!$perms->edit_profile) {
                     $notices[] = 'You\'re not allowed to edit your profile';
                 } else {
-                    $profileFieldInfos = iterator_to_array($profileFields->getFields());
+                    $profileFieldInfos = iterator_to_array($msz->profileFields->getFields());
                     $profileFieldsSetInfos = [];
                     $profileFieldsSetValues = [];
                     $profileFieldsRemove = [];
 
                     foreach($profileFieldInfos as $fieldInfo) {
-                        $fieldName = $fieldInfo->getName();
+                        $fieldName = $fieldInfo->name;
                         $fieldValue = empty($profileFieldsSubmit[$fieldName]) ? '' : (string)filter_var($profileFieldsSubmit[$fieldName]);
 
                         if(empty($profileFieldsSubmit[$fieldName])) {
@@ -130,15 +120,15 @@ if($isEditing) {
                             $profileFieldsSetInfos[] = $fieldInfo;
                             $profileFieldsSetValues[] = $fieldValue;
                         } else
-                            $notices[] = sprintf('%s isn\'t properly formatted.', $fieldInfo->getTitle());
+                            $notices[] = sprintf('%s isn\'t properly formatted.', $fieldInfo->title);
 
                         unset($fieldName, $fieldValue, $fieldInfo);
                     }
 
                     if(!empty($profileFieldsRemove))
-                        $profileFields->removeFieldValues($userInfo, $profileFieldsRemove);
+                        $msz->profileFields->removeFieldValues($userInfo, $profileFieldsRemove);
                     if(!empty($profileFieldsSetInfos))
-                        $profileFields->setFieldValues($userInfo, $profileFieldsSetInfos, $profileFieldsSetValues);
+                        $msz->profileFields->setFieldValues($userInfo, $profileFieldsSetInfos, $profileFieldsSetValues);
                 }
             }
 
@@ -148,12 +138,12 @@ if($isEditing) {
                 } else {
                     $aboutText  = (string)($_POST['about']['text'] ?? '');
                     $aboutParse = (int)($_POST['about']['parser'] ?? Parser::PLAIN);
-                    $aboutValid = $users->validateProfileAbout($aboutParse, $aboutText);
+                    $aboutValid = $msz->usersCtx->users->validateProfileAbout($aboutParse, $aboutText);
 
                     if($aboutValid === '')
-                        $users->updateUser($userInfo, aboutContent: $aboutText, aboutParser: $aboutParse);
+                        $msz->usersCtx->users->updateUser($userInfo, aboutBody: $aboutText, aboutBodyParser: $aboutParse);
                     else
-                        $notices[] = $users->validateProfileAboutText($aboutValid);
+                        $notices[] = $msz->usersCtx->users->validateProfileAboutText($aboutValid);
                 }
             }
 
@@ -163,12 +153,12 @@ if($isEditing) {
                 } else {
                     $sigText  = (string)($_POST['signature']['text'] ?? '');
                     $sigParse = (int)($_POST['signature']['parser'] ?? Parser::PLAIN);
-                    $sigValid = $users->validateForumSignature($sigParse, $sigText);
+                    $sigValid = $msz->usersCtx->users->validateForumSignature($sigParse, $sigText);
 
                     if($sigValid === '')
-                        $users->updateUser($userInfo, signatureContent: $sigText, signatureParser: $sigParse);
+                        $msz->usersCtx->users->updateUser($userInfo, signatureBody: $sigText, signatureBodyParser: $sigParse);
                     else
-                        $notices[] = $users->validateForumSignatureText($sigValid);
+                        $notices[] = $msz->usersCtx->users->validateForumSignatureText($sigValid);
                 }
             }
 
@@ -179,12 +169,12 @@ if($isEditing) {
                     $birthYear  = (int)($_POST['birthdate']['year'] ?? 0);
                     $birthMonth = (int)($_POST['birthdate']['month'] ?? 0);
                     $birthDay   = (int)($_POST['birthdate']['day'] ?? 0);
-                    $birthValid = $users->validateBirthdate($birthYear, $birthMonth, $birthDay);
+                    $birthValid = $msz->usersCtx->users->validateBirthdate($birthYear, $birthMonth, $birthDay);
 
                     if($birthValid === '')
-                        $users->updateUser($userInfo, birthYear: $birthYear, birthMonth: $birthMonth, birthDay: $birthDay);
+                        $msz->usersCtx->users->updateUser($userInfo, birthYear: $birthYear, birthMonth: $birthMonth, birthDay: $birthDay);
                     else
-                        $notices[] = $users->validateBirthdateText($birthValid);
+                        $notices[] = $msz->usersCtx->users->validateBirthdateText($birthValid);
                 }
             }
 
@@ -281,7 +271,7 @@ if($isEditing) {
                     }
                 }
 
-                $users->updateUser($userInfo, backgroundSettings: $backgroundInfo->getSettings());
+                $msz->usersCtx->users->updateUser($userInfo, backgroundSettings: $backgroundInfo->getSettings());
             }
         }
 
@@ -293,12 +283,12 @@ if($isEditing) {
 
 // TODO: create user counters so these can be statically kept
 $profileStats = new stdClass;
-$profileStats->forum_topic_count = $forumCtx->countTotalUserTopics($userInfo);
-$profileStats->forum_post_count = $forumCtx->countTotalUserPosts($userInfo);
-$profileStats->comments_count = $msz->getComments()->countPosts(userInfo: $userInfo, deleted: false);
+$profileStats->forum_topic_count = $msz->forumCtx->countTotalUserTopics($userInfo);
+$profileStats->forum_post_count = $msz->forumCtx->countTotalUserPosts($userInfo);
+$profileStats->comments_count = $msz->comments->countPosts(userInfo: $userInfo, deleted: false);
 
 if(!$viewingAsGuest) {
-    Template::set('profile_warnings', iterator_to_array($usersCtx->getWarnings()->getWarningsWithDefaultBacklog($userInfo)));
+    Template::set('profile_warnings', iterator_to_array($msz->usersCtx->warnings->getWarningsWithDefaultBacklog($userInfo)));
 
     if((!$isBanned || $canEdit)) {
         $unranked = $cfg->getValues([
@@ -306,25 +296,25 @@ if(!$viewingAsGuest) {
             'forum_leader.unranked.topic:a',
         ]);
 
-        $activeCategoryStats = $forumCategories->getMostActiveCategoryInfo(
+        $activeCategoryStats = $msz->forumCtx->categories->getMostActiveCategoryInfo(
             $userInfo,
             $unranked['forum_leader.unranked.forum'],
             $unranked['forum_leader.unranked.topic'],
             deleted: false
         );
-        $activeCategoryInfo = $activeCategoryStats->success ? $forumCategories->getCategory(categoryId: $activeCategoryStats->categoryId) : null;
+        $activeCategoryInfo = $activeCategoryStats->success ? $msz->forumCtx->categories->getCategory(categoryId: $activeCategoryStats->categoryId) : null;
 
-        $activeTopicStats = $forumTopics->getMostActiveTopicInfo(
+        $activeTopicStats = $msz->forumCtx->topics->getMostActiveTopicInfo(
             $userInfo,
             $unranked['forum_leader.unranked.forum'],
             $unranked['forum_leader.unranked.topic'],
             deleted: false
         );
-        $activeTopicInfo = $activeTopicStats->success ? $forumTopics->getTopic(topicId: $activeTopicStats->topicId) : null;
+        $activeTopicInfo = $activeTopicStats->success ? $msz->forumCtx->topics->getTopic(topicId: $activeTopicStats->topicId) : null;
 
-        $profileFieldValues = iterator_to_array($profileFields->getFieldValues($userInfo));
-        $profileFieldInfos = $profileFieldInfos ?? iterator_to_array($profileFields->getFields(fieldValueInfos: $isEditing ? null : $profileFieldValues));
-        $profileFieldFormats = iterator_to_array($profileFields->getFieldFormats(fieldValueInfos: $profileFieldValues));
+        $profileFieldValues = iterator_to_array($msz->profileFields->getFieldValues($userInfo));
+        $profileFieldInfos = $profileFieldInfos ?? iterator_to_array($msz->profileFields->getFields(fieldValueInfos: $isEditing ? null : $profileFieldValues));
+        $profileFieldFormats = iterator_to_array($msz->profileFields->getFieldFormats(fieldValueInfos: $profileFieldValues));
 
         $profileFieldRawValues = [];
         $profileFieldLinkValues = [];
@@ -335,24 +325,24 @@ if(!$viewingAsGuest) {
             unset($fieldValue);
 
             foreach($profileFieldValues as $fieldValueTest)
-                if($fieldValueTest->getFieldId() === $fieldInfo->getId()) {
+                if($fieldValueTest->fieldId === $fieldInfo->id) {
                     $fieldValue = $fieldValueTest;
                     break;
                 }
 
-            $fieldName = $fieldInfo->getName();
+            $fieldName = $fieldInfo->name;
 
             if(isset($fieldValue)) {
                 foreach($profileFieldFormats as $fieldFormatTest)
-                    if($fieldFormatTest->getId() === $fieldValue->getFormatId()) {
+                    if($fieldFormatTest->id === $fieldValue->formatId) {
                         $fieldFormat = $fieldFormatTest;
                         break;
                     }
 
-                $profileFieldRawValues[$fieldName] = $fieldValue->getValue();
-                $profileFieldDisplayValues[$fieldName] = $fieldFormat->formatDisplay($fieldValue->getValue());
-                if($fieldFormat->hasLinkFormat())
-                    $profileFieldLinkValues[$fieldName] = $fieldFormat->formatLink($fieldValue->getValue());
+                $profileFieldRawValues[$fieldName] = $fieldValue->value;
+                $profileFieldDisplayValues[$fieldName] = $fieldFormat->formatDisplay($fieldValue->value);
+                if($fieldFormat->linkFormat !== null)
+                    $profileFieldLinkValues[$fieldName] = $fieldFormat->formatLink($fieldValue->value);
             }
         }
 
@@ -372,7 +362,7 @@ if(!$viewingAsGuest) {
 Template::render('profile.index', [
     'profile_viewer' => $viewerInfo,
     'profile_user' => $userInfo,
-    'profile_colour' => $usersCtx->getUserColour($userInfo),
+    'profile_colour' => $msz->usersCtx->getUserColour($userInfo),
     'profile_stats' => $profileStats,
     'profile_mode' => $profileMode,
     'profile_notices' => $notices,
diff --git a/public-legacy/search.php b/public-legacy/search.php
index 6c6d3c6a..7ce50ed3 100644
--- a/public-legacy/search.php
+++ b/public-legacy/search.php
@@ -6,8 +6,7 @@ use RuntimeException;
 use Index\XArray;
 use Misuzu\Comments\CommentsCategory;
 
-$authInfo = $msz->getAuthInfo();
-if(!$authInfo->isLoggedIn())
+if(!$msz->authInfo->isLoggedIn)
     Template::throwError(403);
 
 $searchQuery = !empty($_GET['q']) && is_string($_GET['q']) ? $_GET['q'] : '';
@@ -24,7 +23,7 @@ Template::addFunction('search_merge_query', function($attrs) use (&$searchQueryE
     if(!empty($attrs['author']))
         $existing[] = 'author:' . $attrs['author'];
     elseif(!empty($searchQueryEvaluated['author']))
-        $existing[] = 'author:' . $searchQueryEvaluated['author']->getName();
+        $existing[] = 'author:' . $searchQueryEvaluated['author']->name;
 
     if(!empty($attrs['after']))
         $existing[] = 'after:' . $attrs['after'];
@@ -39,15 +38,6 @@ Template::addFunction('search_merge_query', function($attrs) use (&$searchQueryE
     return implode(' ', $existing);
 });
 if(!empty($searchQuery)) {
-    $usersCtx = $msz->getUsersContext();
-    $users = $usersCtx->getUsers();
-    $forumCtx = $msz->getForumContext();
-    $forumCategories = $forumCtx->getCategories();
-    $forumTopics = $forumCtx->getTopics();
-    $forumPosts = $forumCtx->getPosts();
-    $news = $msz->getNews();
-    $comments = $msz->getComments();
-
     $searchQueryAttributes = ['type', 'author', 'after'];
     $searchQueryParts = explode(' ', $searchQuery);
     foreach($searchQueryParts as $queryPart) {
@@ -72,60 +62,60 @@ if(!empty($searchQuery)) {
 
     if(!empty($searchQueryEvaluated['author']))
         try {
-            $searchQueryEvaluated['author'] = $usersCtx->getUserInfo($searchQueryEvaluated['author'], 'search');
+            $searchQueryEvaluated['author'] = $msz->usersCtx->getUserInfo($searchQueryEvaluated['author'], 'search');
         } catch(RuntimeException $ex) {
             unset($searchQueryEvaluated['author']);
         }
 
     if(empty($searchQueryEvaluated['type']) || str_starts_with($searchQueryEvaluated['type'], 'forum')) {
-        $currentUser = $authInfo->getUserInfo();
+        $currentUser = $msz->authInfo->userInfo;
         $currentUserId = $currentUser === null ? 0 : (int)$currentUser->getId();
 
         $forumCategoryIds = XArray::where(
-            $forumCategories->getCategories(hidden: false),
-            fn($categoryInfo) => $categoryInfo->mayHaveTopics() && $authInfo->getPerms('forum', $categoryInfo)->check(Perm::F_CATEGORY_VIEW)
+            $msz->forumCtx->categories->getCategories(hidden: false),
+            fn($categoryInfo) => $categoryInfo->mayHaveTopics && $msz->authInfo->getPerms('forum', $categoryInfo)->check(Perm::F_CATEGORY_VIEW)
         );
 
-        $forumTopicInfos = $forumTopics->getTopics(categoryInfo: $forumCategoryIds, deleted: false, searchQuery: $searchQueryEvaluated);
+        $forumTopicInfos = $msz->forumCtx->topics->getTopics(categoryInfo: $forumCategoryIds, deleted: false, searchQuery: $searchQueryEvaluated);
         $ftopics = [];
 
         foreach($forumTopicInfos as $topicInfo) {
             $ftopics[] = $topic = new stdClass;
             $topic->info = $topicInfo;
-            $topic->unread = $forumTopics->checkTopicUnread($topicInfo, $currentUser);
-            $topic->participated = $forumTopics->checkTopicParticipated($topicInfo, $currentUser);
+            $topic->unread = $msz->forumCtx->topics->checkTopicUnread($topicInfo, $currentUser);
+            $topic->participated = $msz->forumCtx->topics->checkTopicParticipated($topicInfo, $currentUser);
             $topic->lastPost = new stdClass;
 
-            if($topicInfo->hasUserId()) {
-                $topic->user = $usersCtx->getUserInfo($topicInfo->getUserId(), 'id');
-                $topic->colour = $usersCtx->getUserColour($topic->user);
+            if($topicInfo->userId !== null) {
+                $topic->user = $msz->usersCtx->getUserInfo($topicInfo->userId, 'id');
+                $topic->colour = $msz->usersCtx->getUserColour($topic->user);
             }
 
             try {
-                $topic->lastPost->info = $lastPostInfo = $forumPosts->getPost(
+                $topic->lastPost->info = $lastPostInfo = $msz->forumCtx->posts->getPost(
                     topicInfo: $topicInfo,
                     getLast: true,
-                    deleted: $topicInfo->isDeleted() ? null : false,
+                    deleted: $topicInfo->deleted ? null : false,
                 );
 
-                if($lastPostInfo->hasUserId()) {
-                    $topic->lastPost->user = $usersCtx->getUserInfo($lastPostInfo->getUserId(), 'id');
-                    $topic->lastPost->colour = $usersCtx->getUserColour($topic->lastPost->user);
+                if($lastPostInfo->userId !== null) {
+                    $topic->lastPost->user = $msz->usersCtx->getUserInfo($lastPostInfo->userId, 'id');
+                    $topic->lastPost->colour = $msz->usersCtx->getUserColour($topic->lastPost->user);
                 }
             } catch(RuntimeException $ex) {}
         }
 
-        $forumPostInfos = $forumPosts->getPosts(categoryInfo: $forumCategoryIds, searchQuery: $searchQueryEvaluated);
+        $forumPostInfos = $msz->forumCtx->posts->getPosts(categoryInfo: $forumCategoryIds, searchQuery: $searchQueryEvaluated);
         $fposts = [];
 
         foreach($forumPostInfos as $postInfo) {
             $fposts[] = $post = new stdClass;
             $post->info = $postInfo;
 
-            if($postInfo->hasUserId()) {
-                $post->user = $usersCtx->getUserInfo($postInfo->getUserId(), 'id');
-                $post->colour = $usersCtx->getUserColour($post->user);
-                $post->postsCount = $forumCtx->countTotalUserPosts($post->user);
+            if($postInfo->userId !== null) {
+                $post->user = $msz->usersCtx->getUserInfo($postInfo->userId, 'id');
+                $post->colour = $msz->usersCtx->getUserColour($post->user);
+                $post->postsCount = $msz->forumCtx->countTotalUserPosts($post->user);
             }
 
             // can't be bothered sorry
@@ -135,22 +125,21 @@ if(!empty($searchQuery)) {
     }
 
     $newsPosts = [];
-    $newsPostInfos = empty($searchQueryEvaluated['type']) || $searchQueryEvaluated['type'] === 'news' ? $news->getPosts(searchQuery: $searchQuery) : [];
+    $newsPostInfos = empty($searchQueryEvaluated['type']) || $searchQueryEvaluated['type'] === 'news' ? $msz->news->getPosts(searchQuery: $searchQuery) : [];
     $newsCategoryInfos = [];
 
     foreach($newsPostInfos as $postInfo) {
-        $userId = $postInfo->getUserId();
-        $categoryId = $postInfo->getCategoryId();
-        $userInfo = $postInfo->hasUserId() ? $usersCtx->getUserInfo($postInfo->getUserId()) : null;
-        $userColour = $usersCtx->getUserColour($userInfo);
+        $categoryId = $postInfo->categoryId;
+        $userInfo = $postInfo->userId !== null ? $msz->usersCtx->getUserInfo($postInfo->userId) : null;
+        $userColour = $msz->usersCtx->getUserColour($userInfo);
 
         if(array_key_exists($categoryId, $newsCategoryInfos))
             $categoryInfo = $newsCategoryInfos[$categoryId];
         else
-            $newsCategoryInfos[$categoryId] = $categoryInfo = $news->getCategory(postInfo: $postInfo);
+            $newsCategoryInfos[$categoryId] = $categoryInfo = $msz->news->getCategory(postInfo: $postInfo);
 
-        $commentsCount = $postInfo->hasCommentsCategoryId()
-            ? $comments->countPosts(categoryInfo: $postInfo->getCommentsCategoryId(), deleted: false) : 0;
+        $commentsCount = $postInfo->commentsSectionId !== null
+            ? $msz->comments->countPosts(categoryInfo: $postInfo->commentsSectionId, deleted: false) : 0;
 
         $newsPosts[] = [
             'post' => $postInfo,
@@ -162,14 +151,14 @@ if(!empty($searchQuery)) {
     }
 
     $members = [];
-    $memberInfos = $users->getUsers(searchQuery: $searchQueryEvaluated);
+    $memberInfos = $msz->usersCtx->users->getUsers(searchQuery: $searchQueryEvaluated);
 
     foreach($memberInfos as $memberInfo) {
         $members[] = $member = new stdClass;
         $member->info = $memberInfo;
-        $member->colour = $usersCtx->getUserColour($memberInfo);
-        $member->ftopics = $forumCtx->countTotalUserTopics($memberInfo);
-        $member->fposts = $forumCtx->countTotalUserPosts($memberInfo);
+        $member->colour = $msz->usersCtx->getUserColour($memberInfo);
+        $member->ftopics = $msz->forumCtx->countTotalUserTopics($memberInfo);
+        $member->fposts = $msz->forumCtx->countTotalUserPosts($memberInfo);
     }
 }
 
diff --git a/public-legacy/settings/account.php b/public-legacy/settings/account.php
index 35d16251..b621c4ce 100644
--- a/public-legacy/settings/account.php
+++ b/public-legacy/settings/account.php
@@ -6,39 +6,35 @@ use Misuzu\Users\User;
 use chillerlan\QRCode\QRCode;
 use chillerlan\QRCode\QROptions;
 
-$authInfo = $msz->getAuthInfo();
-if(!$authInfo->isLoggedIn())
+if(!$msz->authInfo->isLoggedIn)
     Template::throwError(401);
 
 $errors = [];
-$usersCtx = $msz->getUsersContext();
-$users = $usersCtx->getUsers();
-$roles = $usersCtx->getRoles();
-$userInfo = $authInfo->getUserInfo();
-$isRestricted = $usersCtx->hasActiveBan($userInfo);
+$userInfo = $msz->authInfo->userInfo;
+$isRestricted = $msz->usersCtx->hasActiveBan($userInfo);
 $isVerifiedRequest = CSRF::validateRequest();
 
 if(!$isRestricted && $isVerifiedRequest && !empty($_POST['role'])) {
     try {
-        $roleInfo = $roles->getRole(($_POST['role']['id'] ?? 0));
+        $roleInfo = $msz->usersCtx->roles->getRole(($_POST['role']['id'] ?? 0));
     } catch(RuntimeException $ex) {}
 
-    if(empty($roleInfo) || !$users->hasRole($userInfo, $roleInfo))
+    if(empty($roleInfo) || !$msz->usersCtx->users->hasRole($userInfo, $roleInfo))
         $errors[] = "You're trying to modify a role that hasn't been assigned to you.";
     else {
         switch($_POST['role']['mode'] ?? '') {
             case 'display':
-                $users->updateUser(
+                $msz->usersCtx->users->updateUser(
                     $userInfo,
                     displayRoleInfo: $roleInfo
                 );
                 break;
 
             case 'leave':
-                if($roleInfo->isLeavable()) {
-                    $users->removeRoles($userInfo, $roleInfo);
-                    $msz->getPerms()->precalculatePermissions(
-                        $msz->getForumContext()->getCategories(),
+                if($roleInfo->leavable) {
+                    $msz->usersCtx->users->removeRoles($userInfo, $roleInfo);
+                    $msz->perms->precalculatePermissions(
+                        $msz->forumCtx->categories,
                         [$userInfo->getId()]
                     );
                 } else
@@ -48,17 +44,17 @@ if(!$isRestricted && $isVerifiedRequest && !empty($_POST['role'])) {
     }
 }
 
-if($isVerifiedRequest && isset($_POST['tfa']['enable']) && $userInfo->hasTOTPKey() !== (bool)$_POST['tfa']['enable']) {
+if($isVerifiedRequest && isset($_POST['tfa']['enable']) && $userInfo->hasTOTP !== (bool)$_POST['tfa']['enable']) {
     $totpKey = '';
 
     if((bool)$_POST['tfa']['enable']) {
         $totpKey = TOTPGenerator::generateKey();
-        $totpIssuer = $msz->getSiteInfo()->getName();
+        $totpIssuer = $msz->siteInfo->name;
         $totpQrcode = (new QRCode(new QROptions([
             'version'    => 5,
             'outputType' => QRCode::OUTPUT_IMAGE_JPG,
             'eccLevel'   => QRCode::ECC_L,
-        ])))->render(sprintf('otpauth://totp/%s:%s?%s', $totpIssuer, $userInfo->getName(), http_build_query([
+        ])))->render(sprintf('otpauth://totp/%s:%s?%s', $totpIssuer, $userInfo->name, http_build_query([
             'secret' => $totpKey,
             'issuer' => $totpIssuer,
         ])));
@@ -69,7 +65,7 @@ if($isVerifiedRequest && isset($_POST['tfa']['enable']) && $userInfo->hasTOTPKey
         ]);
     }
 
-    $users->updateUser(userInfo: $userInfo, totpKey: $totpKey);
+    $msz->usersCtx->users->updateUser(userInfo: $userInfo, totpKey: $totpKey);
 }
 
 if($isVerifiedRequest && !empty($_POST['current_password'])) {
@@ -80,15 +76,15 @@ if($isVerifiedRequest && !empty($_POST['current_password'])) {
         if(!empty($_POST['email']['new'])) {
             if(empty($_POST['email']['confirm']) || $_POST['email']['new'] !== $_POST['email']['confirm']) {
                 $errors[] = 'The addresses you entered did not match each other.';
-            } elseif($userInfo->getEMailAddress() === mb_strtolower($_POST['email']['confirm'])) {
+            } elseif($userInfo->emailAddress === mb_strtolower($_POST['email']['confirm'])) {
                 $errors[] = 'This is already your e-mail address!';
             } else {
-                $checkMail = $users->validateEMailAddress($_POST['email']['new']);
+                $checkMail = $msz->usersCtx->users->validateEMailAddress($_POST['email']['new']);
 
                 if($checkMail !== '') {
-                    $errors[] = $users->validateEMailAddressText($checkMail);
+                    $errors[] = $msz->usersCtx->users->validateEMailAddressText($checkMail);
                 } else {
-                    $users->updateUser(userInfo: $userInfo, emailAddr: $_POST['email']['new']);
+                    $msz->usersCtx->users->updateUser(userInfo: $userInfo, emailAddr: $_POST['email']['new']);
                     $msz->createAuditLog('PERSONAL_EMAIL_CHANGE', [$_POST['email']['new']]);
                 }
             }
@@ -99,12 +95,12 @@ if($isVerifiedRequest && !empty($_POST['current_password'])) {
             if(empty($_POST['password']['confirm']) || $_POST['password']['new'] !== $_POST['password']['confirm']) {
                 $errors[] = 'The new passwords you entered did not match each other.';
             } else {
-                $checkPassword = $users->validatePassword($_POST['password']['new']);
+                $checkPassword = $msz->usersCtx->users->validatePassword($_POST['password']['new']);
 
                 if($checkPassword !== '') {
-                    $errors[] = $users->validatePasswordText($checkPassword);
+                    $errors[] = $msz->usersCtx->users->validatePasswordText($checkPassword);
                 } else {
-                    $users->updateUser(userInfo: $userInfo, password: $_POST['password']['new']);
+                    $msz->usersCtx->users->updateUser(userInfo: $userInfo, password: $_POST['password']['new']);
                     $msz->createAuditLog('PERSONAL_PASSWORD_CHANGE');
                 }
             }
@@ -114,9 +110,9 @@ if($isVerifiedRequest && !empty($_POST['current_password'])) {
 
 // reload $userInfo object
 if($_SERVER['REQUEST_METHOD'] === 'POST' && $isVerifiedRequest)
-    $userInfo = $users->getUser($userInfo->getId(), 'id');
+    $userInfo = $msz->usersCtx->users->getUser($userInfo->getId(), 'id');
 
-$userRoles = iterator_to_array($roles->getRoles(userInfo: $userInfo));
+$userRoles = iterator_to_array($msz->usersCtx->roles->getRoles(userInfo: $userInfo));
 
 Template::render('settings.account', [
     'errors' => $errors,
diff --git a/public-legacy/settings/data.php b/public-legacy/settings/data.php
index e4002fa1..a8f95d75 100644
--- a/public-legacy/settings/data.php
+++ b/public-legacy/settings/data.php
@@ -5,11 +5,10 @@ use ZipArchive;
 use Index\XString;
 use Misuzu\Users\UserInfo;
 
-$authInfo = $msz->getAuthInfo();
-if(!$authInfo->isLoggedIn())
+if(!$msz->authInfo->isLoggedIn)
     Template::throwError(401);
 
-$dbConn = $msz->getDbConn();
+$dbConn = $msz->dbConn;
 
 function db_to_zip(ZipArchive $archive, UserInfo $userInfo, string $baseName, array $fieldInfos, string $userIdField = 'user_id'): string {
     global $dbConn;
@@ -98,7 +97,7 @@ function db_to_zip(ZipArchive $archive, UserInfo $userInfo, string $baseName, ar
 }
 
 $errors = [];
-$userInfo = $authInfo->getUserInfo();
+$userInfo = $msz->authInfo->userInfo;
 
 if(isset($_POST['action']) && is_string($_POST['action'])) {
     if(isset($_POST['password']) && is_string($_POST['password'])
diff --git a/public-legacy/settings/logs.php b/public-legacy/settings/logs.php
index 235dc3b8..b8e970e3 100644
--- a/public-legacy/settings/logs.php
+++ b/public-legacy/settings/logs.php
@@ -3,20 +3,15 @@ namespace Misuzu;
 
 use Misuzu\Pagination;
 
-$authInfo = $msz->getAuthInfo();
-$currentUser = $authInfo->getUserInfo();
+$currentUser = $msz->authInfo->userInfo;
 if($currentUser === null)
     Template::throwError(401);
 
-$authCtx = $msz->getAuthContext();
-$loginAttempts = $authCtx->getLoginAttempts();
-$auditLog = $msz->getAuditLog();
+$loginHistoryPagination = new Pagination($msz->authCtx->loginAttempts->countAttempts(userInfo: $currentUser), 5, 'hp');
+$accountLogPagination = new Pagination($msz->auditLog->countLogs(userInfo: $currentUser), 10, 'ap');
 
-$loginHistoryPagination = new Pagination($loginAttempts->countAttempts(userInfo: $currentUser), 5, 'hp');
-$accountLogPagination = new Pagination($auditLog->countLogs(userInfo: $currentUser), 10, 'ap');
-
-$loginHistory = iterator_to_array($loginAttempts->getAttempts(userInfo: $currentUser, pagination: $loginHistoryPagination));
-$auditLogs = iterator_to_array($auditLog->getLogs(userInfo: $currentUser, pagination: $accountLogPagination));
+$loginHistory = iterator_to_array($msz->authCtx->loginAttempts->getAttempts(userInfo: $currentUser, pagination: $loginHistoryPagination));
+$auditLogs = iterator_to_array($msz->auditLog->getLogs(userInfo: $currentUser, pagination: $accountLogPagination));
 
 Template::render('settings.logs', [
     'login_history_list' => $loginHistory,
diff --git a/public-legacy/settings/sessions.php b/public-legacy/settings/sessions.php
index ab366786..45ef5c92 100644
--- a/public-legacy/settings/sessions.php
+++ b/public-legacy/settings/sessions.php
@@ -3,15 +3,12 @@ namespace Misuzu;
 
 use RuntimeException;
 
-$authInfo = $msz->getAuthInfo();
-if(!$authInfo->isLoggedIn())
+if(!$msz->authInfo->isLoggedIn)
     Template::throwError(401);
 
 $errors = [];
-$authCtx = $msz->getAuthContext();
-$sessions = $authCtx->getSessions();
-$currentUser = $authInfo->getUserInfo();
-$activeSessionId = $authInfo->getSessionId();
+$currentUser = $msz->authInfo->userInfo;
+$activeSessionId = $msz->authInfo->sessionId;
 
 while($_SERVER['REQUEST_METHOD'] === 'POST' && CSRF::validateRequest()) {
     $sessionId = (string)filter_input(INPUT_POST, 'session');
@@ -19,38 +16,38 @@ while($_SERVER['REQUEST_METHOD'] === 'POST' && CSRF::validateRequest()) {
 
     if($sessionId === 'all') {
         $activeSessionKilled = true;
-        $sessions->deleteSessions(userInfos: $currentUser);
+        $msz->authCtx->sessions->deleteSessions(userInfos: $currentUser);
         $msz->createAuditLog('PERSONAL_SESSION_DESTROY_ALL');
     } else {
         try {
-            $sessionInfo = $sessions->getSession(sessionId: $sessionId);
+            $sessionInfo = $msz->authCtx->sessions->getSession(sessionId: $sessionId);
         } catch(RuntimeException $ex) {}
 
-        if(empty($sessionInfo) || $sessionInfo->getUserId() !== $currentUser->getId()) {
+        if(empty($sessionInfo) || $sessionInfo->userId !== $currentUser->getId()) {
             $errors[] = "That session doesn't exist.";
             break;
         }
 
-        $activeSessionKilled = $sessionInfo->getId() === $activeSessionId;
-        $sessions->deleteSessions(sessionInfos: $sessionInfo);
-        $msz->createAuditLog('PERSONAL_SESSION_DESTROY', [$sessionInfo->getId()]);
+        $activeSessionKilled = $sessionInfo->id === $activeSessionId;
+        $msz->authCtx->sessions->deleteSessions(sessionInfos: $sessionInfo);
+        $msz->createAuditLog('PERSONAL_SESSION_DESTROY', [$sessionInfo->id]);
     }
 
     if($activeSessionKilled) {
-        Tools::redirect($msz->getUrls()->format('index'));
+        Tools::redirect($msz->urls->format('index'));
         return;
     } else break;
 }
 
-$pagination = new Pagination($sessions->countSessions(userInfo: $currentUser), 10);
+$pagination = new Pagination($msz->authCtx->sessions->countSessions(userInfo: $currentUser), 10);
 
 $sessionList = [];
-$sessionInfos = $sessions->getSessions(userInfo: $currentUser, pagination: $pagination);
+$sessionInfos = $msz->authCtx->sessions->getSessions(userInfo: $currentUser, pagination: $pagination);
 
 foreach($sessionInfos as $sessionInfo)
     $sessionList[] = [
         'info' => $sessionInfo,
-        'active' => $sessionInfo->getId() === $activeSessionId,
+        'active' => $sessionInfo->id === $activeSessionId,
     ];
 
 Template::render('settings.sessions', [
diff --git a/public/index.php b/public/index.php
index 29a91f08..d9d3a4b2 100644
--- a/public/index.php
+++ b/public/index.php
@@ -38,7 +38,7 @@ if(file_exists(MSZ_ROOT . '/.migrating')) {
     exit;
 }
 
-$tokenPacker = $msz->getAuthContext()->createAuthTokenPacker();
+$tokenPacker = $msz->authCtx->createAuthTokenPacker();
 
 if(filter_has_var(INPUT_COOKIE, 'msz_auth'))
     $tokenInfo = $tokenPacker->unpack(filter_input(INPUT_COOKIE, 'msz_auth'));
@@ -55,32 +55,30 @@ $userInfo = null;
 $sessionInfo = null;
 $userInfoReal = null;
 
-if($tokenInfo->hasUserId() && $tokenInfo->hasSessionToken()) {
-    $users = $msz->getUsersContext()->getUsers();
-    $sessions = $msz->getAuthContext()->getSessions();
+if($tokenInfo->hasUserId && $tokenInfo->hasSessionToken) {
     $tokenBuilder = new AuthTokenBuilder($tokenInfo);
 
     try {
-        $sessionInfo = $sessions->getSession(sessionToken: $tokenInfo->getSessionToken());
+        $sessionInfo = $msz->authCtx->sessions->getSession(sessionToken: $tokenInfo->sessionToken);
 
-        if($sessionInfo->hasExpired()) {
+        if($sessionInfo->expired) {
             $tokenBuilder->removeUserId();
             $tokenBuilder->removeSessionToken();
-        } elseif($sessionInfo->getUserId() === $tokenInfo->getUserId()) {
-            $userInfo = $users->getUser($tokenInfo->getUserId(), 'id');
+        } elseif($sessionInfo->userId === $tokenInfo->userId) {
+            $userInfo = $msz->usersCtx->users->getUser($tokenInfo->userId, 'id');
 
-            if($userInfo->isDeleted()) {
+            if($userInfo->deleted) {
                 $tokenBuilder->removeUserId();
                 $tokenBuilder->removeSessionToken();
             } else {
-                $users->recordUserActivity($userInfo, remoteAddr: $_SERVER['REMOTE_ADDR']);
-                $sessions->recordSessionActivity(sessionInfo: $sessionInfo, remoteAddr: $_SERVER['REMOTE_ADDR']);
-                if($sessionInfo->shouldBumpExpires())
+                $msz->usersCtx->users->recordUserActivity($userInfo, remoteAddr: $_SERVER['REMOTE_ADDR']);
+                $msz->authCtx->sessions->recordSessionActivity(sessionInfo: $sessionInfo, remoteAddr: $_SERVER['REMOTE_ADDR']);
+                if($sessionInfo->shouldBumpExpires)
                     $tokenBuilder->setEdited();
 
-                if($tokenInfo->hasImpersonatedUserId()) {
-                    $allowToImpersonate = $userInfo->isSuperUser();
-                    $impersonatedUserId = $tokenInfo->getImpersonatedUserId();
+                if($tokenInfo->hasImpersonatedUserId) {
+                    $allowToImpersonate = $userInfo->super;
+                    $impersonatedUserId = $tokenInfo->impersonatedUserId;
 
                     if(!$allowToImpersonate) {
                         $allowImpersonateUsers = $cfg->getArray(sprintf('impersonate.allow.u%s', $userInfo->getId()));
@@ -91,7 +89,7 @@ if($tokenInfo->hasUserId() && $tokenInfo->hasSessionToken()) {
                         $userInfoReal = $userInfo;
 
                         try {
-                            $userInfo = $users->getUser($impersonatedUserId, 'id');
+                            $userInfo = $msz->usersCtx->users->getUser($impersonatedUserId, 'id');
                         } catch(RuntimeException $ex) {
                             $userInfo = $userInfoReal;
                             $userInfoReal = null;
@@ -116,12 +114,11 @@ if($tokenInfo->hasUserId() && $tokenInfo->hasSessionToken()) {
     }
 }
 
-$authInfo = $msz->getAuthInfo();
-$authInfo->setInfo($tokenInfo, $userInfo, $sessionInfo, $userInfoReal);
+$msz->authInfo->setInfo($tokenInfo, $userInfo, $sessionInfo, $userInfoReal);
 
 CSRF::init(
     $cfg->getString('csrf.secret', 'soup'),
-    ($authInfo->isLoggedIn() ? $sessionInfo->getToken() : $_SERVER['REMOTE_ADDR'])
+    ($msz->authInfo->isLoggedIn ? $sessionInfo->token : $_SERVER['REMOTE_ADDR'])
 );
 
 // order for these two currently matters i think: it shouldn't.
diff --git a/src/AuditLog/AuditLogInfo.php b/src/AuditLog/AuditLogInfo.php
index 77c9195b..5b85f7e9 100644
--- a/src/AuditLog/AuditLogInfo.php
+++ b/src/AuditLog/AuditLogInfo.php
@@ -7,12 +7,12 @@ use Index\Db\DbResult;
 
 class AuditLogInfo {
     public function __construct(
-        private ?string $userId,
-        private string $action,
-        private array $params,
-        private int $created,
-        private string $address,
-        private string $country,
+        public private(set) ?string $userId,
+        public private(set) string $action,
+        public private(set) array $params,
+        public private(set) int $createdTime,
+        public private(set) string $remoteAddress,
+        public private(set) string $countryCode,
     ) {}
 
     public static function fromResult(DbResult $result): AuditLogInfo {
@@ -20,51 +20,25 @@ class 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),
+            createdTime: $result->getInteger(3),
+            remoteAddress: $result->isNull(4) ? '::1' : $result->getString(4), // apparently this being NULL is possible?
+            countryCode: $result->getString(5),
         );
     }
 
-    public function hasUserId(): bool {
-        return $this->userId !== null;
+    public CarbonImmutable $createdAt {
+        get => CarbonImmutable::createFromTimestampUTC($this->createdTime);
     }
 
-    public function getUserId(): ?string {
-        return $this->userId;
-    }
+    public string $formatted {
+        get {
+            if(array_key_exists($this->action, self::FORMATS))
+                try {
+                    return vsprintf(self::FORMATS[$this->action], $this->params);
+                } catch(ValueError $ex) {}
 
-    public function getAction(): string {
-        return $this->action;
-    }
-
-    public function getParams(): array {
-        return $this->params;
-    }
-
-    public function getCreatedTime(): int {
-        return $this->created;
-    }
-
-    public function getCreatedAt(): CarbonImmutable {
-        return CarbonImmutable::createFromTimestampUTC($this->created);
-    }
-
-    public function getRemoteAddress(): string {
-        return $this->address;
-    }
-
-    public function getCountryCode(): string {
-        return $this->country;
-    }
-
-    public function getFormatted(): string {
-        if(array_key_exists($this->action, self::FORMATS))
-            try {
-                return vsprintf(self::FORMATS[$this->action], $this->params);
-            } catch(ValueError $ex) {}
-
-        return sprintf('%s(%s)', $this->action, implode(', ', $this->params));
+            return sprintf('%s(%s)', $this->action, implode(', ', $this->params));
+        }
     }
 
     public const FORMATS = [
diff --git a/src/Auth/AuthContext.php b/src/Auth/AuthContext.php
index 450f00d9..5ba2966f 100644
--- a/src/Auth/AuthContext.php
+++ b/src/Auth/AuthContext.php
@@ -5,10 +5,10 @@ use Index\Config\Config;
 use Index\Db\DbConnection;
 
 class AuthContext {
-    private Sessions $sessions;
-    private LoginAttempts $loginAttempts;
-    private RecoveryTokens $recoveryTokens;
-    private TwoFactorAuthSessions $tfaSessions;
+    public private(set) Sessions $sessions;
+    public private(set) LoginAttempts $loginAttempts;
+    public private(set) RecoveryTokens $recoveryTokens;
+    public private(set) TwoFactorAuthSessions $tfaSessions;
 
     private Config $config;
 
@@ -20,22 +20,6 @@ class AuthContext {
         $this->tfaSessions = new TwoFactorAuthSessions($dbConn);
     }
 
-    public function getSessions(): Sessions {
-        return $this->sessions;
-    }
-
-    public function getLoginAttempts(): LoginAttempts {
-        return $this->loginAttempts;
-    }
-
-    public function getRecoveryTokens(): RecoveryTokens {
-        return $this->recoveryTokens;
-    }
-
-    public function getTwoFactorAuthSessions(): TwoFactorAuthSessions {
-        return $this->tfaSessions;
-    }
-
     public function createAuthTokenPacker(): AuthTokenPacker {
         return new AuthTokenPacker($this->config->getString('secret', 'meow'));
     }
diff --git a/src/Auth/AuthInfo.php b/src/Auth/AuthInfo.php
index d5ec1579..a009025a 100644
--- a/src/Auth/AuthInfo.php
+++ b/src/Auth/AuthInfo.php
@@ -8,16 +8,16 @@ use Misuzu\Perms\Permissions;
 use Misuzu\Users\UserInfo;
 
 class AuthInfo {
-    private Permissions $permissions;
-    private AuthTokenInfo $tokenInfo;
-    private ?UserInfo $userInfo;
-    private ?SessionInfo $sessionInfo;
-    private ?UserInfo $realUserInfo;
-    private array $perms;
+    public private(set) AuthTokenInfo $tokenInfo;
+    public private(set) ?UserInfo $userInfo;
+    public private(set) ?SessionInfo $sessionInfo;
+    public private(set) ?UserInfo $realUserInfo;
+    public private(set) array $perms;
 
-    public function __construct(Permissions $permissions) {
-        $this->permissions = $permissions;
-        $this->setInfo(AuthTokenInfo::empty());
+    public function __construct(
+        private Permissions $permissions
+    ) {
+        $this->removeInfo();
     }
 
     public function setInfo(
@@ -37,40 +37,24 @@ class AuthInfo {
         $this->setInfo(AuthTokenInfo::empty());
     }
 
-    public function getTokenInfo(): AuthTokenInfo {
-        return $this->tokenInfo;
+    public bool $isLoggedIn {
+        get => $this->userInfo !== null;
     }
 
-    public function isLoggedIn(): bool {
-        return $this->userInfo !== null;
+    public ?string $userId {
+        get => $this->userInfo?->getId();
     }
 
-    public function getUserId(): ?string {
-        return $this->userInfo?->getId();
+    public ?string $sessionId {
+        get => $this->sessionInfo?->id;
     }
 
-    public function getUserInfo(): ?UserInfo {
-        return $this->userInfo;
+    public bool $isImpersonating {
+        get => $this->realUserInfo !== null;
     }
 
-    public function getSessionId(): ?string {
-        return $this->sessionInfo?->getId();
-    }
-
-    public function getSessionInfo(): ?SessionInfo {
-        return $this->sessionInfo;
-    }
-
-    public function isImpersonating(): bool {
-        return $this->realUserInfo !== null;
-    }
-
-    public function getRealUserId(): ?string {
-        return $this->realUserInfo?->getId();
-    }
-
-    public function getRealUserInfo(): ?UserInfo {
-        return $this->realUserInfo;
+    public ?string $realUserId {
+        get => $this->realUserInfo?->getId();
     }
 
     public function getPerms(
@@ -79,7 +63,7 @@ class AuthInfo {
     ): IPermissionResult {
         $cacheKey = $category;
         if($forumCategoryInfo !== null)
-            $cacheKey .= '|' . ($forumCategoryInfo instanceof ForumCategoryInfo ? $forumCategoryInfo->getId() : $forumCategoryInfo);
+            $cacheKey .= '|' . ($forumCategoryInfo instanceof ForumCategoryInfo ? $forumCategoryInfo->id : $forumCategoryInfo);
 
         if(array_key_exists($cacheKey, $this->perms))
             return $this->perms[$cacheKey];
diff --git a/src/Auth/AuthRpcHandler.php b/src/Auth/AuthRpcHandler.php
index ef03b88a..88482f9e 100644
--- a/src/Auth/AuthRpcHandler.php
+++ b/src/Auth/AuthRpcHandler.php
@@ -16,7 +16,7 @@ final class AuthRpcHandler implements RpcHandler {
     ) {}
 
     private function canImpersonateUserId(UserInfo $impersonator, string $targetId): bool {
-        if($impersonator->isSuperUser())
+        if($impersonator->super)
             return true;
 
         $whitelist = $this->impersonateConfig->getArray(sprintf('allow.u%s', $impersonator->getId()));
@@ -26,30 +26,28 @@ final class AuthRpcHandler implements RpcHandler {
     #[RpcAction('misuzu:auth:attemptMisuzuAuth')]
     public function procAttemptMisuzuAuth(string $remoteAddr, string $token): array {
         $tokenInfo = $this->authCtx->createAuthTokenPacker()->unpack($token);
-        if(!$tokenInfo->isEmpty())
-            $token = $tokenInfo->getSessionToken();
+        if(!$tokenInfo->isEmpty)
+            $token = $tokenInfo->sessionToken;
 
-        $sessions = $this->authCtx->getSessions();
         try {
-            $sessionInfo = $sessions->getSession(sessionToken: $token);
+            $sessionInfo = $this->authCtx->sessions->getSession(sessionToken: $token);
         } catch(RuntimeException $ex) {
             return ['method' => 'misuzu', 'error' => 'token'];
         }
 
-        if($sessionInfo->hasExpired()) {
-            $sessions->deleteSessions(sessionInfos: $sessionInfo);
+        if($sessionInfo->expired) {
+            $this->authCtx->sessions->deleteSessions(sessionInfos: $sessionInfo);
             return ['method' => 'misuzu', 'error' => 'expired'];
         }
 
-        $sessions->recordSessionActivity(sessionInfo: $sessionInfo, remoteAddr: $remoteAddr);
+        $this->authCtx->sessions->recordSessionActivity(sessionInfo: $sessionInfo, remoteAddr: $remoteAddr);
 
-        $users = $this->usersCtx->getUsers();
-        $userInfo = $users->getUser($sessionInfo->getUserId(), 'id');
-        if($tokenInfo->hasImpersonatedUserId() && $this->canImpersonateUserId($userInfo, $tokenInfo->getImpersonatedUserId())) {
+        $userInfo = $this->usersCtx->users->getUser($sessionInfo->userId, 'id');
+        if($tokenInfo->hasImpersonatedUserId && $this->canImpersonateUserId($userInfo, $tokenInfo->impersonatedUserId)) {
             $userInfoReal = $userInfo;
 
             try {
-                $userInfo = $users->getUser($tokenInfo->getImpersonatedUserId(), 'id');
+                $userInfo = $this->usersCtx->users->getUser($tokenInfo->impersonatedUserId, 'id');
             } catch(RuntimeException $ex) {
                 $userInfo = $userInfoReal;
             }
@@ -59,7 +57,7 @@ final class AuthRpcHandler implements RpcHandler {
             'method' => 'misuzu',
             'type' => 'user',
             'user' => $userInfo->getId(),
-            'expires' => $sessionInfo->getExpiresTime(),
+            'expires' => $sessionInfo->expiresTime,
         ];
     }
 }
diff --git a/src/Auth/AuthTokenBuilder.php b/src/Auth/AuthTokenBuilder.php
index 0624d80e..1cbbe613 100644
--- a/src/Auth/AuthTokenBuilder.php
+++ b/src/Auth/AuthTokenBuilder.php
@@ -46,7 +46,7 @@ class AuthTokenBuilder {
 
     public function setSessionToken(SessionInfo|string $sessionKey): void {
         if($sessionKey instanceof SessionInfo)
-            $sessionKey = $sessionKey->getToken();
+            $sessionKey = $sessionKey->token;
 
         $this->setProperty(AuthTokenInfo::SESSION_TOKEN, $sessionKey);
     }
diff --git a/src/Auth/AuthTokenInfo.php b/src/Auth/AuthTokenInfo.php
index a07092a8..18f8aec1 100644
--- a/src/Auth/AuthTokenInfo.php
+++ b/src/Auth/AuthTokenInfo.php
@@ -10,18 +10,10 @@ class AuthTokenInfo {
     public const IMPERSONATED_USER_ID = 'i'; // Impersonated user ID
 
     public function __construct(
-        private int $timestamp = 0,
+        public private(set) int $timestamp = 0,
         private array $props = []
     ) {}
 
-    public function isEmpty(): bool {
-        return $this->timestamp === 0 && empty($this->props);
-    }
-
-    public function getTimestamp(): int {
-        return $this->timestamp;
-    }
-
     public function getProperties(): array {
         return $this->props;
     }
@@ -38,35 +30,41 @@ class AuthTokenInfo {
         return $this->props[$name] ?? '';
     }
 
-    public function hasUserId(): bool {
-        return $this->hasProperty(self::USER_ID);
+    public bool $isEmpty {
+        get => $this->timestamp === 0 && empty($this->props);
     }
 
-    public function getUserId(): string {
-        return $this->getProperty(self::USER_ID);
+    public bool $hasUserId {
+        get => $this->hasProperty(self::USER_ID);
     }
 
-    public function hasSessionToken(): bool {
-        return $this->hasProperty(self::SESSION_TOKEN)
+    public string $userId {
+        get => $this->getProperty(self::USER_ID);
+    }
+
+    public bool $hasSessionToken {
+        get => $this->hasProperty(self::SESSION_TOKEN)
             || $this->hasProperty(self::SESSION_TOKEN_HEX);
     }
 
-    public function getSessionToken(): string {
-        if($this->hasProperty(self::SESSION_TOKEN))
-            return $this->getProperty(self::SESSION_TOKEN);
+    public string $sessionToken {
+        get {
+            if($this->hasProperty(self::SESSION_TOKEN))
+                return $this->getProperty(self::SESSION_TOKEN);
 
-        if($this->hasProperty(self::SESSION_TOKEN_HEX))
-            return bin2hex($this->getProperty(self::SESSION_TOKEN_HEX));
+            if($this->hasProperty(self::SESSION_TOKEN_HEX))
+                return bin2hex($this->getProperty(self::SESSION_TOKEN_HEX));
 
-        return '';
+            return '';
+        }
     }
 
-    public function hasImpersonatedUserId(): bool {
-        return $this->hasProperty(self::IMPERSONATED_USER_ID);
+    public bool $hasImpersonatedUserId {
+        get => $this->hasProperty(self::IMPERSONATED_USER_ID);
     }
 
-    public function getImpersonatedUserId(): string {
-        return $this->getProperty(self::IMPERSONATED_USER_ID);
+    public string $impersonatedUserId {
+        get => $this->getProperty(self::IMPERSONATED_USER_ID);
     }
 
     private static AuthTokenInfo $empty;
diff --git a/src/Auth/AuthTokenPacker.php b/src/Auth/AuthTokenPacker.php
index fd9a346c..079fc845 100644
--- a/src/Auth/AuthTokenPacker.php
+++ b/src/Auth/AuthTokenPacker.php
@@ -11,7 +11,7 @@ class AuthTokenPacker {
 
     public function pack(AuthTokenBuilder|AuthTokenInfo $tokenInfo): string {
         $props = $tokenInfo->getProperties();
-        $timestamp = $tokenInfo instanceof AuthTokenInfo ? $tokenInfo->getTimestamp() : time();
+        $timestamp = $tokenInfo instanceof AuthTokenInfo ? $tokenInfo->timestamp : time();
 
         $data = '';
 
diff --git a/src/Auth/LoginAttemptInfo.php b/src/Auth/LoginAttemptInfo.php
index bd24da7d..52828229 100644
--- a/src/Auth/LoginAttemptInfo.php
+++ b/src/Auth/LoginAttemptInfo.php
@@ -7,64 +7,32 @@ use Index\Db\DbResult;
 
 class LoginAttemptInfo {
     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 private(set) ?string $userId,
+        public private(set) bool $success,
+        public private(set) string $remoteAddress,
+        public private(set) string $countryCode,
+        public private(set) int $createdTime,
+        public private(set) string $userAgentString,
+        public private(set) string $clientInfoJson,
     ) {}
 
     public static function fromResult(DbResult $result): LoginAttemptInfo {
         return new LoginAttemptInfo(
             userId: $result->getStringOrNull(0),
             success: $result->getBoolean(1),
-            remoteAddr: $result->getString(2),
+            remoteAddress: $result->getString(2),
             countryCode: $result->getString(3),
-            created: $result->getInteger(4),
-            userAgent: $result->getString(5),
-            clientInfo: $result->getString(6),
+            createdTime: $result->getInteger(4),
+            userAgentString: $result->getString(5),
+            clientInfoJson: $result->getString(6),
         );
     }
 
-    public function hasUserId(): bool {
-        return $this->userId !== null;
+    public CarbonImmutable $createdAt {
+        get => CarbonImmutable::createFromTimestampUTC($this->createdTime);
     }
 
-    public function getUserId(): ?string {
-        return $this->userId;
-    }
-
-    public function isSuccess(): bool {
-        return $this->success;
-    }
-
-    public function getRemoteAddress(): string {
-        return $this->remoteAddr;
-    }
-
-    public function getCountryCode(): string {
-        return $this->countryCode;
-    }
-
-    public function getCreatedTime(): int {
-        return $this->created;
-    }
-
-    public function getCreatedAt(): CarbonImmutable {
-        return CarbonImmutable::createFromTimestampUTC($this->created);
-    }
-
-    public function getUserAgentString(): string {
-        return $this->userAgent;
-    }
-
-    public function getClientInfoRaw(): string {
-        return $this->clientInfo;
-    }
-
-    public function getClientInfo(): ClientInfo {
-        return ClientInfo::decode($this->clientInfo);
+    public ClientInfo $clientInfo {
+        get => ClientInfo::decode($this->clientInfoJson);
     }
 }
diff --git a/src/Auth/RecoveryTokenInfo.php b/src/Auth/RecoveryTokenInfo.php
index fccacf1b..890a0879 100644
--- a/src/Auth/RecoveryTokenInfo.php
+++ b/src/Auth/RecoveryTokenInfo.php
@@ -7,55 +7,43 @@ use Index\Db\DbResult;
 class RecoveryTokenInfo {
     public const LIFETIME = 60 * 60;
 
-    private string $userId;
-    private string $remoteAddr;
-    private int $created;
-    private ?string $code;
+    public function __construct(
+        public private(set) string $userId,
+        public private(set) string $remoteAddress,
+        public private(set) int $createdTime,
+        #[\SensitiveParameter] public private(set) ?string $code
+    ) {}
 
-    public function __construct(DbResult $result) {
-        $this->userId = (string)$result->getInteger(0);
-        $this->remoteAddr = $result->getString(1);
-        $this->created = $result->getInteger(2);
-        $this->code = $result->isNull(3) ? null : $result->getString(3);
+    public static function fromResult(DbResult $result): RecoveryTokenInfo {
+        return new RecoveryTokenInfo(
+            userId: $result->getString(0),
+            remoteAddress: $result->getString(1),
+            createdTime: $result->getInteger(2),
+            code: $result->getStringOrNull(3),
+        );
     }
 
-    public function getUserId(): string {
-        return $this->userId;
+    public CarbonImmutable $createdAt {
+        get => CarbonImmutable::createFromTimestampUTC($this->createdTime);
     }
 
-    public function getRemoteAddress(): string {
-        return $this->remoteAddr;
+    public int $expiresTime {
+        get => $this->createdTime + self::LIFETIME;
     }
 
-    public function getCreatedTime(): int {
-        return $this->created;
+    public CarbonImmutable $expiresAt {
+        get => CarbonImmutable::createFromTimestampUTC($this->expiresTime);
     }
 
-    public function getCreatedAt(): CarbonImmutable {
-        return CarbonImmutable::createFromTimestampUTC($this->created);
+    public bool $expired {
+        get => $this->expiresTime <= time();
     }
 
-    public function getExpiresTime(): int {
-        return $this->created + self::LIFETIME;
+    public bool $hasCode {
+        get => $this->code !== null;
     }
 
-    public function getExpiresAt(): CarbonImmutable {
-        return CarbonImmutable::createFromTimestampUTC($this->getExpiresTime());
-    }
-
-    public function hasExpired(): bool {
-        return $this->getExpiresTime() <= time();
-    }
-
-    public function hasCode(): bool {
-        return $this->code !== null;
-    }
-
-    public function getCode(): ?string {
-        return $this->code;
-    }
-
-    public function isValid(): bool {
-        return $this->hasCode() && !$this->hasExpired();
+    public bool $isValid {
+        get => $this->hasCode && !$this->expired;
     }
 }
diff --git a/src/Auth/RecoveryTokens.php b/src/Auth/RecoveryTokens.php
index 798891cb..e4ede85e 100644
--- a/src/Auth/RecoveryTokens.php
+++ b/src/Auth/RecoveryTokens.php
@@ -67,7 +67,7 @@ class RecoveryTokens {
         if(!$result->next())
             throw new RuntimeException('Recovery token not found.');
 
-        return new RecoveryTokenInfo($result);
+        return RecoveryTokenInfo::fromResult($result);
     }
 
     public function createToken(
@@ -89,12 +89,12 @@ class RecoveryTokens {
     }
 
     public function invalidateToken(RecoveryTokenInfo $tokenInfo): void {
-        if(!$tokenInfo->hasCode())
+        if(!$tokenInfo->hasCode)
             return;
 
         $stmt = $this->cache->get('UPDATE msz_users_password_resets SET verification_code = NULL WHERE verification_code = ? AND user_id = ?');
-        $stmt->addParameter(1, $tokenInfo->getCode());
-        $stmt->addParameter(2, $tokenInfo->getUserId());
+        $stmt->addParameter(1, $tokenInfo->code);
+        $stmt->addParameter(2, $tokenInfo->userId);
         $stmt->execute();
     }
 }
diff --git a/src/Auth/SessionInfo.php b/src/Auth/SessionInfo.php
index 03ae2d5b..1995ffe9 100644
--- a/src/Auth/SessionInfo.php
+++ b/src/Auth/SessionInfo.php
@@ -7,18 +7,18 @@ use Index\Db\DbResult;
 
 class SessionInfo {
     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 private(set) string $id,
+        public private(set) string $userId,
+        #[\SensitiveParameter] public private(set) string $token,
+        public private(set) string $firstRemoteAddress,
+        public private(set) ?string $lastRemoteAddress,
+        public private(set) string $userAgentString,
+        public private(set) string $clientInfoJson,
+        public private(set) string $countryCode,
+        public private(set) int $expiresTime,
+        public private(set) bool $shouldBumpExpires,
+        public private(set) int $createdTime,
+        public private(set) ?int $lastActiveTime,
     ) {}
 
     public static function fromResult(DbResult $result): SessionInfo {
@@ -26,91 +26,36 @@ class 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),
+            firstRemoteAddress: $result->getString(3),
+            lastRemoteAddress: $result->getStringOrNull(4),
+            userAgentString: $result->getString(5),
+            clientInfoJson: $result->getString(6),
             countryCode: $result->getString(7),
-            expires: $result->getInteger(8),
-            bumpExpires: $result->getBoolean(9),
-            created: $result->getInteger(10),
-            lastActive: $result->getIntegerOrNull(11),
+            expiresTime: $result->getInteger(8),
+            shouldBumpExpires: $result->getBoolean(9),
+            createdTime: $result->getInteger(10),
+            lastActiveTime: $result->getIntegerOrNull(11),
         );
     }
 
-    public function getId(): string {
-        return $this->id;
+    public ClientInfo $clientInfo {
+        get => ClientInfo::decode($this->clientInfoJson);
     }
 
-    public function getUserId(): string {
-        return $this->userId;
+    public CarbonImmutable $expiresAt {
+        get => CarbonImmutable::createFromTimestampUTC($this->expiresTime);
     }
 
-    public function getToken(): string {
-        return $this->token;
+    public bool $expired {
+        get => $this->expiresTime < time();
     }
 
-    public function getFirstRemoteAddress(): string {
-        return $this->firstRemoteAddr;
+    public CarbonImmutable $createdAt {
+        get => CarbonImmutable::createFromTimestampUTC($this->createdTime);
     }
 
-    public function hasLastRemoteAddress(): bool {
-        return $this->lastRemoteAddr !== null;
-    }
-
-    public function getLastRemoteAddress(): ?string {
-        return $this->lastRemoteAddr;
-    }
-
-    public function getUserAgentString(): string {
-        return $this->userAgent;
-    }
-
-    public function getClientInfoRaw(): string {
-        return $this->clientInfo;
-    }
-
-    public function getClientInfo(): ClientInfo {
-        return ClientInfo::decode($this->clientInfo);
-    }
-
-    public function getCountryCode(): string {
-        return $this->countryCode;
-    }
-
-    public function getExpiresTime(): int {
-        return $this->expires;
-    }
-
-    public function getExpiresAt(): CarbonImmutable {
-        return CarbonImmutable::createFromTimestampUTC($this->expires);
-    }
-
-    public function shouldBumpExpires(): bool {
-        return $this->bumpExpires;
-    }
-
-    public function hasExpired(): bool {
-        return $this->expires < time();
-    }
-
-    public function getCreatedTime(): int {
-        return $this->created;
-    }
-
-    public function getCreatedAt(): CarbonImmutable {
-        return CarbonImmutable::createFromTimestampUTC($this->created);
-    }
-
-    public function hasLastActive(): bool {
-        return $this->lastActive !== null;
-    }
-
-    public function getLastActiveTime(): ?int {
-        return $this->lastActive;
-    }
-
-    public function getLastActiveAt(): ?CarbonImmutable {
-        return $this->lastActive === null ? null : CarbonImmutable::createFromTimestampUTC($this->lastActive);
+    public ?CarbonImmutable $lastActiveAt {
+        get => $this->lastActiveTime === null
+            ? null : CarbonImmutable::createFromTimestampUTC($this->lastActiveTime);
     }
 }
diff --git a/src/Auth/Sessions.php b/src/Auth/Sessions.php
index d1623d9d..850d6196 100644
--- a/src/Auth/Sessions.php
+++ b/src/Auth/Sessions.php
@@ -207,7 +207,7 @@ class Sessions {
         if($hasSessionInfos)
             foreach($sessionInfos as $sessionInfo) {
                 if($sessionInfo instanceof SessionInfo)
-                    $sessionInfo = $sessionInfo->getId();
+                    $sessionInfo = $sessionInfo->id;
                 elseif(!is_string($sessionInfo))
                     throw new InvalidArgumentException('$sessionInfos must be strings or instances of SessionInfo.');
 
@@ -245,7 +245,7 @@ class Sessions {
         if($sessionInfo !== null && $sessionToken !== null)
             throw new InvalidArgumentException('Only one of $sessionInfo and $sessionToken may be set at once.');
         if($sessionInfo instanceof SessionInfo)
-            $sessionInfo = $sessionInfo->getId();
+            $sessionInfo = $sessionInfo->id;
 
         $hasSessionInfo = $sessionInfo !== null;
         $hasSessionToken = $sessionToken !== null;
diff --git a/src/Changelog/ChangeInfo.php b/src/Changelog/ChangeInfo.php
index f32d05ba..6081e334 100644
--- a/src/Changelog/ChangeInfo.php
+++ b/src/Changelog/ChangeInfo.php
@@ -6,74 +6,42 @@ use Index\Db\DbResult;
 
 class ChangeInfo {
     public function __construct(
-        private string $id,
-        private ?string $userId,
-        private int $action,
-        private int $created,
-        private string $summary,
-        private string $body,
+        public private(set) string $id,
+        public private(set) ?string $userId,
+        public private(set) int $actionId,
+        public private(set) int $createdTime,
+        public private(set) string $summary,
+        public private(set) string $body,
     ) {}
 
     public static function fromResult(DbResult $result): ChangeInfo {
         return new ChangeInfo(
             id: $result->getString(0),
             userId: $result->getStringOrNull(1),
-            action: $result->getInteger(2),
-            created: $result->getInteger(3),
+            actionId: $result->getInteger(2),
+            createdTime: $result->getInteger(3),
             summary: $result->getString(4),
             body: $result->getString(5),
         );
     }
 
-    public function getId(): string {
-        return $this->id;
+    public string $action {
+        get => Changelog::convertFromActionId($this->actionId);
     }
 
-    public function hasUserId(): bool {
-        return $this->userId !== null;
+    public string $actionText {
+        get => Changelog::actionText($this->actionId);
     }
 
-    public function getUserId(): ?string {
-        return $this->userId;
+    public CarbonImmutable $createdAt {
+        get => CarbonImmutable::createFromTimestampUTC($this->createdTime);
     }
 
-    public function getActionId(): int {
-        return $this->action;
+    public string $date {
+        get => gmdate('Y-m-d', $this->createdTime);
     }
 
-    public function getAction(): string {
-        return Changelog::convertFromActionId($this->action);
-    }
-
-    public function getActionText(): string {
-        return Changelog::actionText($this->action);
-    }
-
-    public function getCreatedTime(): int {
-        return $this->created;
-    }
-
-    public function getCreatedAt(): CarbonImmutable {
-        return CarbonImmutable::createFromTimestampUTC($this->created);
-    }
-
-    public function getDate(): string {
-        return gmdate('Y-m-d', $this->created);
-    }
-
-    public function getSummary(): string {
-        return $this->summary;
-    }
-
-    public function hasBody(): bool {
-        return !empty($this->body);
-    }
-
-    public function getBody(): string {
-        return $this->body;
-    }
-
-    public function getCommentsCategoryName(): string {
-        return sprintf('changelog-date-%s', $this->getDate());
+    public string $commentsCategoryName {
+        get => sprintf('changelog-date-%s', $this->date);
     }
 }
diff --git a/src/Changelog/ChangeTagInfo.php b/src/Changelog/ChangeTagInfo.php
index 0ad2c7ac..69c4bffb 100644
--- a/src/Changelog/ChangeTagInfo.php
+++ b/src/Changelog/ChangeTagInfo.php
@@ -6,56 +6,36 @@ use Carbon\CarbonImmutable;
 use Index\Db\DbResult;
 
 class ChangeTagInfo implements Stringable {
-    private string $id;
-    private string $name;
-    private string $description;
-    private int $created;
-    private int $archived;
-    private int $changes;
+    public function __construct(
+        public private(set) string $id,
+        public private(set) string $name,
+        public private(set) string $description,
+        public private(set) int $createdTime,
+        public private(set) ?int $archivedTime,
+        public private(set) int $changesCount, // this is virtual!!!
+    ) {}
 
-    public function __construct(DbResult $result) {
-        $this->id = (string)$result->getInteger(0);
-        $this->name = $result->getString(1);
-        $this->description = $result->getString(2);
-        $this->created = $result->getInteger(3);
-        $this->archived = $result->getInteger(4);
-        $this->changes = $result->getInteger(5);
+    public static function fromResult(DbResult $result): ChangeTagInfo {
+        return new ChangeTagInfo(
+            id: $result->getString(0),
+            name: $result->getString(1),
+            description: $result->getString(2),
+            createdTime: $result->getInteger(3),
+            archivedTime: $result->getIntegerOrNull(4),
+            changesCount: $result->getInteger(5),
+        );
     }
 
-    public function getId(): string {
-        return $this->id;
+    public CarbonImmutable $createdAt {
+        get => CarbonImmutable::createFromTimestampUTC($this->createdTime);
     }
 
-    public function getName(): string {
-        return $this->name;
+    public CarbonImmutable $archivedAt {
+        get => CarbonImmutable::createFromTimestampUTC($this->archivedTime);
     }
 
-    public function getDescription(): string {
-        return $this->description;
-    }
-
-    public function getCreatedTime(): int {
-        return $this->created;
-    }
-
-    public function getCreatedAt(): CarbonImmutable {
-        return CarbonImmutable::createFromTimestampUTC($this->created);
-    }
-
-    public function getArchivedTime(): int {
-        return $this->archived;
-    }
-
-    public function getArchivedAt(): CarbonImmutable {
-        return CarbonImmutable::createFromTimestampUTC($this->archived);
-    }
-
-    public function isArchived(): bool {
-        return $this->archived > 0;
-    }
-
-    public function getChangesCount(): int {
-        return $this->changes;
+    public bool $archived {
+        get => $this->archivedTime !== null;
     }
 
     public function __toString(): string {
diff --git a/src/Changelog/Changelog.php b/src/Changelog/Changelog.php
index f9edfadb..aa0ce4bb 100644
--- a/src/Changelog/Changelog.php
+++ b/src/Changelog/Changelog.php
@@ -216,7 +216,7 @@ class Changelog {
 
     public function deleteChange(ChangeInfo|string $infoOrId): void {
         if($infoOrId instanceof ChangeInfo)
-            $infoOrId = $infoOrId->getId();
+            $infoOrId = $infoOrId->id;
 
         $stmt = $this->cache->get('DELETE FROM msz_changelog_changes WHERE change_id = ?');
         $stmt->addParameter(1, $infoOrId);
@@ -233,7 +233,7 @@ class Changelog {
         DateTimeInterface|int|null $createdAt = null
     ): void {
         if($infoOrId instanceof ChangeInfo)
-            $infoOrId = $infoOrId->getId();
+            $infoOrId = $infoOrId->id;
 
         if(is_string($action))
             $action = self::convertToActionId($action);
@@ -271,7 +271,7 @@ class Changelog {
         ChangeInfo|string|null $changeInfo = null
     ): array {
         if($changeInfo instanceof ChangeInfo)
-            $changeInfo = $changeInfo->getId();
+            $changeInfo = $changeInfo->id;
 
         $hasChangeInfo = $changeInfo !== null;
 
@@ -292,7 +292,7 @@ class Changelog {
             if(array_key_exists($tagId, $this->tags))
                 $tags[] = $this->tags[$tagId];
             else
-                $tags[] = $this->tags[$tagId] = new ChangeTagInfo($result);
+                $tags[] = $this->tags[$tagId] = ChangeTagInfo::fromResult($result);
         }
 
         return $tags;
@@ -307,7 +307,7 @@ class Changelog {
         if(!$result->next())
             throw new RuntimeException('No tag with that ID exists.');
 
-        return new ChangeTagInfo($result);
+        return ChangeTagInfo::fromResult($result);
     }
 
     public function createTag(
@@ -334,7 +334,7 @@ class Changelog {
 
     public function deleteTag(ChangeTagInfo|string $infoOrId): void {
         if($infoOrId instanceof ChangeTagInfo)
-            $infoOrId = $infoOrId->getId();
+            $infoOrId = $infoOrId->id;
 
         $stmt = $this->cache->get('DELETE FROM msz_changelog_tags WHERE tag_id = ?');
         $stmt->addParameter(1, $infoOrId);
@@ -348,7 +348,7 @@ class Changelog {
         ?bool $archived = null
     ): void {
         if($infoOrId instanceof ChangeTagInfo)
-            $infoOrId = $infoOrId->getId();
+            $infoOrId = $infoOrId->id;
 
         if($name !== null) {
             $name = trim($name);
@@ -377,9 +377,9 @@ class Changelog {
 
     public function addTagToChange(ChangeInfo|string $change, ChangeTagInfo|string $tag): void {
         if($change instanceof ChangeInfo)
-            $change = $change->getId();
+            $change = $change->id;
         if($tag instanceof ChangeTagInfo)
-            $tag = $tag->getId();
+            $tag = $tag->id;
 
         $stmt = $this->cache->get('INSERT INTO msz_changelog_change_tags (change_id, tag_id) VALUES (?, ?)');
         $stmt->addParameter(1, $change);
@@ -389,9 +389,9 @@ class Changelog {
 
     public function removeTagFromChange(ChangeInfo|string $change, ChangeTagInfo|string $tag): void {
         if($change instanceof ChangeInfo)
-            $change = $change->getId();
+            $change = $change->id;
         if($tag instanceof ChangeTagInfo)
-            $tag = $tag->getId();
+            $tag = $tag->id;
 
         $stmt = $this->cache->get('DELETE FROM msz_changelog_change_tags WHERE change_id = ? AND tag_id = ?');
         $stmt->addParameter(1, $change);
diff --git a/src/Changelog/ChangelogRoutes.php b/src/Changelog/ChangelogRoutes.php
index 18c23d60..76394200 100644
--- a/src/Changelog/ChangelogRoutes.php
+++ b/src/Changelog/ChangelogRoutes.php
@@ -72,10 +72,10 @@ final class ChangelogRoutes implements RouteHandler, UrlSource {
         $commentsCategoryName = null;
 
         foreach($changeInfos as $changeInfo) {
-            $userInfo = $changeInfo->hasUserId() ? $this->usersCtx->getUserInfo($changeInfo->getUserId()) : null;
+            $userInfo = $changeInfo->userId !== null ? $this->usersCtx->getUserInfo($changeInfo->userId) : null;
 
             if($commentsCategoryName === null)
-                $commentsCategoryName = $changeInfo->getCommentsCategoryName();
+                $commentsCategoryName = $changeInfo->commentsCategoryName;
 
             $changes[] = [
                 'change' => $changeInfo,
@@ -108,14 +108,14 @@ final class ChangelogRoutes implements RouteHandler, UrlSource {
         }
 
         $tagInfos = $this->changelog->getTags(changeInfo: $changeInfo);
-        $userInfo = $changeInfo->hasUserId() ? $this->usersCtx->getUserInfo($changeInfo->getUserId()) : null;
+        $userInfo = $changeInfo->userId !== null ? $this->usersCtx->getUserInfo($changeInfo->userId) : null;
 
         return Template::renderRaw('changelog.change', [
             'change_info' => $changeInfo,
             'change_tags' => $tagInfos,
             'change_user_info' => $userInfo,
             'change_user_colour' => $this->usersCtx->getUserColour($userInfo),
-            'comments_info' => $this->getCommentsInfo($changeInfo->getCommentsCategoryName()),
+            'comments_info' => $this->getCommentsInfo($changeInfo->commentsCategoryName),
         ]);
     }
 
@@ -124,8 +124,8 @@ final class ChangelogRoutes implements RouteHandler, UrlSource {
     public function getFeed($response) {
         $response->setContentType('application/rss+xml; charset=utf-8');
 
-        $siteName = $this->siteInfo->getName();
-        $siteUrl = $this->siteInfo->getURL();
+        $siteName = $this->siteInfo->name;
+        $siteUrl = $this->siteInfo->url;
         $changes = $this->changelog->getChanges(pagination: new Pagination(10));
 
         $feed = new FeedBuilder;
@@ -136,10 +136,10 @@ final class ChangelogRoutes implements RouteHandler, UrlSource {
 
         foreach($changes as $change)
             $feed->createEntry(function($item) use ($change, $siteUrl) {
-                $item->setTitle(sprintf('%s: %s', $change->getActionText(), $change->getSummary()));
-                $item->setCreatedAt($change->getCreatedTime());
-                $item->setContentUrl($siteUrl . $this->urls->format('changelog-change', ['change' => $change->getId()]));
-                $item->setCommentsUrl($siteUrl . $this->urls->format('changelog-change-comments', ['change' => $change->getId()]));
+                $item->setTitle(sprintf('%s: %s', $change->actionText, $change->summary));
+                $item->setCreatedAt($change->createdTime);
+                $item->setContentUrl($siteUrl . $this->urls->format('changelog-change', ['change' => $change->id]));
+                $item->setCommentsUrl($siteUrl . $this->urls->format('changelog-change-comments', ['change' => $change->id]));
             });
 
         return $feed->toXmlString();
diff --git a/src/Comments/Comments.php b/src/Comments/Comments.php
index df0e14e1..78a2e45e 100644
--- a/src/Comments/Comments.php
+++ b/src/Comments/Comments.php
@@ -8,11 +8,11 @@ use Misuzu\Pagination;
 use Misuzu\Users\UserInfo;
 
 class Comments {
-    private DbConnection $dbConn;
     private DbStatementCache $cache;
 
-    public function __construct(DbConnection $dbConn) {
-        $this->dbConn = $dbConn;
+    public function __construct(
+        private DbConnection $dbConn
+    ) {
         $this->cache = new DbStatementCache($dbConn);
     }
 
@@ -101,7 +101,7 @@ class Comments {
         if($hasPostInfo) {
             if($postInfo instanceof CommentsPostInfo) {
                 $query .= ' WHERE category_id = ?';
-                $value = $postInfo->getCategoryId();
+                $value = $postInfo->categoryId;
             } else {
                 $query .= ' WHERE category_id = (SELECT category_id FROM msz_comments_posts WHERE comment_id = ?)';
                 $value = $postInfo;
@@ -157,7 +157,7 @@ class Comments {
 
     public function deleteCategory(CommentsCategoryInfo|string $category): void {
         if($category instanceof CommentsCategoryInfo)
-            $category = $category->getId();
+            $category = $category->id;
 
         $stmt = $this->cache->get('DELETE FROM msz_comments_categories WHERE category_id = ?');
         $stmt->addParameter(1, $category);
@@ -171,7 +171,7 @@ class Comments {
         UserInfo|string|null $owner = null
     ): void {
         if($category instanceof CommentsCategoryInfo)
-            $category = $category->getId();
+            $category = $category->id;
         if($owner instanceof UserInfo)
             $owner = $owner->getId();
 
@@ -191,7 +191,7 @@ class Comments {
 
     public function lockCategory(CommentsCategoryInfo|string $category): void {
         if($category instanceof CommentsCategoryInfo)
-            $category = $category->getId();
+            $category = $category->id;
 
         $stmt = $this->cache->get('UPDATE msz_comments_categories SET category_locked = COALESCE(category_locked, NOW()) WHERE category_id = ?');
         $stmt->addParameter(1, $category);
@@ -200,7 +200,7 @@ class Comments {
 
     public function unlockCategory(CommentsCategoryInfo|string $category): void {
         if($category instanceof CommentsCategoryInfo)
-            $category = $category->getId();
+            $category = $category->id;
 
         $stmt = $this->cache->get('UPDATE msz_comments_categories SET category_locked = NULL WHERE category_id = ?');
         $stmt->addParameter(1, $category);
@@ -215,9 +215,9 @@ class Comments {
         ?bool $deleted = null
     ): int {
         if($categoryInfo instanceof CommentsCategoryInfo)
-            $categoryInfo = $categoryInfo->getId();
+            $categoryInfo = $categoryInfo->id;
         if($parentInfo instanceof CommentsPostInfo)
-            $parentInfo = $parentInfo->getId();
+            $parentInfo = $parentInfo->id;
 
         $hasCategoryInfo = $categoryInfo !== null;
         $hasParentInfo = $parentInfo !== null;
@@ -270,9 +270,9 @@ class Comments {
         bool $includeVotesCount = false
     ): iterable {
         if($categoryInfo instanceof CommentsCategoryInfo)
-            $categoryInfo = $categoryInfo->getId();
+            $categoryInfo = $categoryInfo->id;
         if($parentInfo instanceof CommentsPostInfo)
-            $parentInfo = $parentInfo->getId();
+            $parentInfo = $parentInfo->id;
 
         $hasCategoryInfo = $categoryInfo !== null;
         $hasParentInfo = $parentInfo !== null;
@@ -359,13 +359,13 @@ class Comments {
         bool $pin = false
     ): CommentsPostInfo {
         if($category instanceof CommentsCategoryInfo)
-            $category = $category->getId();
+            $category = $category->id;
         if($parent instanceof CommentsPostInfo) {
             if($category === null)
-                $category = $parent->getCategoryId();
-            elseif($category !== $parent->getCategoryId())
+                $category = $parent->categoryId;
+            elseif($category !== $parent->categoryId)
                 throw new InvalidArgumentException('$parent belongs to a different category than where this post is attempted to be created.');
-            $parent = $parent->getId();
+            $parent = $parent->id;
         }
         if($category === null)
             throw new InvalidArgumentException('$category is null; at least a $category or $parent must be specified.');
@@ -387,7 +387,7 @@ class Comments {
 
     public function deletePost(CommentsPostInfo|string $infoOrId): void {
         if($infoOrId instanceof CommentsPostInfo)
-            $infoOrId = $infoOrId->getId();
+            $infoOrId = $infoOrId->id;
 
         $stmt = $this->cache->get('UPDATE msz_comments_posts SET comment_deleted = COALESCE(comment_deleted, NOW()) WHERE comment_id = ?');
         $stmt->addParameter(1, $infoOrId);
@@ -396,7 +396,7 @@ class Comments {
 
     public function nukePost(CommentsPostInfo|string $infoOrId): void {
         if($infoOrId instanceof CommentsPostInfo)
-            $infoOrId = $infoOrId->getId();
+            $infoOrId = $infoOrId->id;
 
         $stmt = $this->cache->get('DELETE FROM msz_comments_posts WHERE comment_id = ?');
         $stmt->addParameter(1, $infoOrId);
@@ -405,7 +405,7 @@ class Comments {
 
     public function restorePost(CommentsPostInfo|string $infoOrId): void {
         if($infoOrId instanceof CommentsPostInfo)
-            $infoOrId = $infoOrId->getId();
+            $infoOrId = $infoOrId->id;
 
         $stmt = $this->cache->get('UPDATE msz_comments_posts SET comment_deleted = NULL WHERE comment_id = ?');
         $stmt->addParameter(1, $infoOrId);
@@ -414,7 +414,7 @@ class Comments {
 
     public function editPost(CommentsPostInfo|string $infoOrId, string $body): void {
         if($infoOrId instanceof CommentsPostInfo)
-            $infoOrId = $infoOrId->getId();
+            $infoOrId = $infoOrId->id;
 
         if(empty(trim($body)))
             throw new InvalidArgumentException('$body may not be empty.');
@@ -427,7 +427,7 @@ class Comments {
 
     public function pinPost(CommentsPostInfo|string $infoOrId): void {
         if($infoOrId instanceof CommentsPostInfo)
-            $infoOrId = $infoOrId->getId();
+            $infoOrId = $infoOrId->id;
 
         $stmt = $this->cache->get('UPDATE msz_comments_posts SET comment_pinned = COALESCE(comment_pinned, NOW()) WHERE comment_id = ?');
         $stmt->addParameter(1, $infoOrId);
@@ -436,7 +436,7 @@ class Comments {
 
     public function unpinPost(CommentsPostInfo|string $infoOrId): void {
         if($infoOrId instanceof CommentsPostInfo)
-            $infoOrId = $infoOrId->getId();
+            $infoOrId = $infoOrId->id;
 
         $stmt = $this->cache->get('UPDATE msz_comments_posts SET comment_pinned = NULL WHERE comment_id = ?');
         $stmt->addParameter(1, $infoOrId);
@@ -448,7 +448,7 @@ class Comments {
         UserInfo|string|null $user
     ): CommentsPostVoteInfo {
         if($post instanceof CommentsPostInfo)
-            $post = $post->getId();
+            $post = $post->id;
         if($user instanceof UserInfo)
             $user = $user->getId();
 
@@ -462,7 +462,7 @@ class Comments {
         if(!$result->next())
             throw new RuntimeException('Failed to fetch vote info.');
 
-        return new CommentsPostVoteInfo($result);
+        return CommentsPostVoteInfo::fromResult($result);
     }
 
     public function addPostVote(
@@ -473,7 +473,7 @@ class Comments {
         if($weight === 0)
             return;
         if($post instanceof CommentsPostInfo)
-            $post = $post->getId();
+            $post = $post->id;
         if($user instanceof UserInfo)
             $user = $user->getId();
 
@@ -497,7 +497,7 @@ class Comments {
         UserInfo|string $user
     ): void {
         if($post instanceof CommentsPostInfo)
-            $post = $post->getId();
+            $post = $post->id;
         if($user instanceof UserInfo)
             $user = $user->getId();
 
diff --git a/src/Comments/CommentsCategoryInfo.php b/src/Comments/CommentsCategoryInfo.php
index 1c29ba0d..6feebb30 100644
--- a/src/Comments/CommentsCategoryInfo.php
+++ b/src/Comments/CommentsCategoryInfo.php
@@ -7,12 +7,12 @@ use Index\Db\DbResult;
 
 class CommentsCategoryInfo {
     public function __construct(
-        private string $id,
-        private string $name,
-        private ?string $ownerId,
-        private int $created,
-        private ?int $locked,
-        private int $comments,
+        public private(set) string $id,
+        public private(set) string $name,
+        public private(set) ?string $ownerId,
+        public private(set) int $createdTime,
+        public private(set) ?int $lockedTime,
+        public private(set) int $commentsCount, // virtual!!
     ) {}
 
     public static function fromResult(DbResult $result): CommentsCategoryInfo {
@@ -20,26 +20,22 @@ class 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),
+            createdTime: $result->getInteger(3),
+            lockedTime: $result->getIntegerOrNull(4),
+            commentsCount: $result->getInteger(5),
         );
     }
 
-    public function getId(): string {
-        return $this->id;
+    public CarbonImmutable $createdAt {
+        get => CarbonImmutable::createFromTimestampUTC($this->createdTime);
     }
 
-    public function getName(): string {
-        return $this->name;
+    public ?CarbonImmutable $lockedAt {
+        get => $this->lockedTime === null ? null : CarbonImmutable::createFromTimestampUTC($this->lockedTime);
     }
 
-    public function hasOwnerId(): bool {
-        return $this->ownerId !== null;
-    }
-
-    public function getOwnerId(): ?string {
-        return $this->ownerId;
+    public bool $locked {
+        get => $this->lockedTime !== null;
     }
 
     public function isOwner(UserInfo|string $user): bool {
@@ -49,29 +45,4 @@ class CommentsCategoryInfo {
             $user = $user->getId();
         return $user === $this->ownerId;
     }
-
-    public function getCreatedTime(): int {
-        return $this->created;
-    }
-
-    public function getCreatedAt(): CarbonImmutable {
-        return CarbonImmutable::createFromTimestampUTC($this->created);
-
-    }
-
-    public function getLockedTime(): ?int {
-        return $this->locked;
-    }
-
-    public function getLockedAt(): ?CarbonImmutable {
-        return $this->locked === null ? null : CarbonImmutable::createFromTimestampUTC($this->locked);
-    }
-
-    public function isLocked(): bool {
-        return $this->locked !== null;
-    }
-
-    public function getCommentsCount(): int {
-        return $this->comments;
-    }
 }
diff --git a/src/Comments/CommentsEx.php b/src/Comments/CommentsEx.php
index b3d0223e..dd83d2ea 100644
--- a/src/Comments/CommentsEx.php
+++ b/src/Comments/CommentsEx.php
@@ -20,8 +20,8 @@ class CommentsEx {
         if(is_string($category))
             $category = $this->comments->ensureCategory($category);
 
-        $hasUser = $this->authInfo->isLoggedIn();
-        $info->user = $hasUser ? $this->authInfo->getUserInfo() : null;
+        $hasUser = $this->authInfo->isLoggedIn;
+        $info->user = $hasUser ? $this->authInfo->userInfo : null;
         $info->colour = $this->usersCtx->getUserColour($info->user);
         $info->perms = $this->authInfo->getPerms('global')->checkMany([
             'can_post' => Perm::G_COMMENTS_CREATE,
@@ -42,7 +42,7 @@ class CommentsEx {
     }
 
     public function decorateComment(CommentsPostInfo $postInfo): object {
-        $userInfo = $postInfo->hasUserId() ? $this->usersCtx->getUserInfo($postInfo->getUserId()) : null;
+        $userInfo = $postInfo->userId !== null ? $this->usersCtx->getUserInfo($postInfo->userId) : null;
 
         $info = new stdClass;
         $info->post = $postInfo;
diff --git a/src/Comments/CommentsPostInfo.php b/src/Comments/CommentsPostInfo.php
index da0f0059..55f17463 100644
--- a/src/Comments/CommentsPostInfo.php
+++ b/src/Comments/CommentsPostInfo.php
@@ -6,19 +6,19 @@ use Index\Db\DbResult;
 
 class CommentsPostInfo {
     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 private(set) string $id,
+        public private(set) string $categoryId,
+        public private(set) ?string $userId,
+        public private(set) ?string $replyingTo,
+        public private(set) string $body,
+        public private(set) int $createdTime,
+        public private(set) ?int $pinnedTime,
+        public private(set) ?int $updatedTime,
+        public private(set) ?int $deletedTime,
+        public private(set) int $repliesCount,
+        public private(set) int $votesCount,
+        public private(set) int $votesPositive,
+        public private(set) int $votesNegative,
     ) {}
 
     public static function fromResult(
@@ -34,15 +34,15 @@ class CommentsPostInfo {
         $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[] = $result->getInteger(++$count); // createdTime
+        $args[] = $result->getIntegerOrNull(++$count); // pinnedTime
+        $args[] = $result->getIntegerOrNull(++$count); // updatedTime
+        $args[] = $result->getIntegerOrNull(++$count); // deletedTime
 
         $args[] = $includeRepliesCount ? $result->getInteger(++$count) : 0;
 
         if($includeVotesCount) {
-            $args[] = $result->getInteger(++$count); // votesTotal
+            $args[] = $result->getInteger(++$count); // votesCount
             $args[] = $result->getInteger(++$count); // votesPositive
             $args[] = $result->getInteger(++$count); // votesNegative
         } else {
@@ -54,99 +54,35 @@ class CommentsPostInfo {
         return new CommentsPostInfo(...$args);
     }
 
-    public function getId(): string {
-        return $this->id;
+    public bool $isReply {
+        get => $this->replyingTo !== null;
     }
 
-    public function getCategoryId(): string {
-        return $this->categoryId;
+    public CarbonImmutable $createdAt {
+        get => CarbonImmutable::createFromTimestampUTC($this->createdTime);
     }
 
-    public function hasUserId(): bool {
-        return $this->userId !== null;
+    public ?CarbonImmutable $pinnedAt {
+        get => $this->pinnedTime === null ? null : CarbonImmutable::createFromTimestampUTC($this->pinnedTime);
     }
 
-    public function getUserId(): ?string {
-        return $this->userId;
+    public bool $pinned {
+        get => $this->pinnedTime !== null;
     }
 
-    public function isReply(): bool {
-        return $this->replyingTo !== null;
+    public ?CarbonImmutable $updatedAt {
+        get => $this->updatedTime === null ? null : CarbonImmutable::createFromTimestampUTC($this->updatedTime);
     }
 
-    public function getReplyingTo(): ?string {
-        return $this->replyingTo;
+    public bool $edited {
+        get => $this->updatedTime !== null;
     }
 
-    public function getBody(): string {
-        return $this->body;
+    public ?CarbonImmutable $deletedAt {
+        get => $this->deletedTime === null ? null : CarbonImmutable::createFromTimestampUTC($this->deletedTime);
     }
 
-    public function getCreatedTime(): int {
-        return $this->created;
-    }
-
-    public function getCreatedAt(): CarbonImmutable {
-        return CarbonImmutable::createFromTimestampUTC($this->created);
-    }
-
-    public function getPinnedTime(): ?int {
-        return $this->pinned;
-    }
-
-    public function getPinnedAt(): ?CarbonImmutable {
-        return $this->pinned === null ? null : CarbonImmutable::createFromTimestampUTC($this->pinned);
-    }
-
-    public function isPinned(): bool {
-        return $this->pinned !== null;
-    }
-
-    public function getUpdatedTime(): ?int {
-        return $this->updated;
-    }
-
-    public function getUpdatedAt(): ?CarbonImmutable {
-        return $this->updated === null ? null : CarbonImmutable::createFromTimestampUTC($this->updated);
-    }
-
-    public function isEdited(): bool {
-        return $this->updated !== null;
-    }
-
-    public function getDeletedTime(): ?int {
-        return $this->deleted;
-    }
-
-    public function getDeletedAt(): ?CarbonImmutable {
-        return $this->deleted === null ? null : CarbonImmutable::createFromTimestampUTC($this->deleted);
-    }
-
-    public function isDeleted(): bool {
-        return $this->deleted !== null;
-    }
-
-    public function hasRepliesCount(): bool {
-        return $this->replies > 0;
-    }
-
-    public function getRepliesCount(): int {
-        return $this->replies;
-    }
-
-    public function hasVotesCount(): bool {
-        return $this->votesTotal > 0;
-    }
-
-    public function getVotesTotal(): int {
-        return $this->votesTotal;
-    }
-
-    public function getVotesPositive(): int {
-        return $this->votesPositive;
-    }
-
-    public function getVotesNegative(): int {
-        return $this->votesNegative;
+    public bool $deleted {
+        get => $this->deletedTime !== null;
     }
 }
diff --git a/src/Comments/CommentsPostVoteInfo.php b/src/Comments/CommentsPostVoteInfo.php
index 250d2c51..246e563b 100644
--- a/src/Comments/CommentsPostVoteInfo.php
+++ b/src/Comments/CommentsPostVoteInfo.php
@@ -4,25 +4,17 @@ namespace Misuzu\Comments;
 use Index\Db\DbResult;
 
 class CommentsPostVoteInfo {
-    private string $commentId;
-    private string $userId;
-    private int $weight;
+    public function __construct(
+        public private(set) string $commentId,
+        public private(set) string $userId,
+        public private(set) int $weight
+    ) {}
 
-    public function __construct(DbResult $result) {
-        $this->commentId = (string)$result->getInteger(0);
-        $this->userId = (string)$result->getInteger(1);
-        $this->weight = $result->getInteger(2);
-    }
-
-    public function getCommentId(): string {
-        return $this->commentId;
-    }
-
-    public function getUserId(): string {
-        return $this->userId;
-    }
-
-    public function getWeight(): int {
-        return $this->weight;
+    public static function fromResult(DbResult $result): CommentsPostVoteInfo {
+        return new CommentsPostVoteInfo(
+            commentId: $result->getString(0),
+            userId: $result->getString(1),
+            weight: $result->getInteger(2),
+        );
     }
 }
diff --git a/src/Counters/CounterInfo.php b/src/Counters/CounterInfo.php
index 980c7823..0632d61c 100644
--- a/src/Counters/CounterInfo.php
+++ b/src/Counters/CounterInfo.php
@@ -6,32 +6,20 @@ use Index\Db\DbResult;
 
 class CounterInfo {
     public function __construct(
-        private string $name,
-        private int $value,
-        private int $updated,
+        public private(set) string $name,
+        public private(set) int $value,
+        public private(set) int $updatedTime,
     ) {}
 
     public static function fromResult(DbResult $result): CounterInfo {
         return new CounterInfo(
             name: $result->getString(0),
             value: $result->getInteger(1),
-            updated: $result->getInteger(2),
+            updatedTime: $result->getInteger(2),
         );
     }
 
-    public function getName(): string {
-        return $this->name;
-    }
-
-    public function getValue(): int {
-        return $this->value;
-    }
-
-    public function getUpdatedTime(): int {
-        return $this->updated;
-    }
-
-    public function getUpdatedAt(): CarbonImmutable {
-        return CarbonImmutable::createFromTimestampUTC($this->updated);
+    public CarbonImmutable $updatedAt {
+        get => CarbonImmutable::createFromTimestampUTC($this->updatedTime);
     }
 }
diff --git a/src/Emoticons/EmoteInfo.php b/src/Emoticons/EmoteInfo.php
index 0e8a0037..3f35f96e 100644
--- a/src/Emoticons/EmoteInfo.php
+++ b/src/Emoticons/EmoteInfo.php
@@ -6,37 +6,21 @@ use Index\Db\DbResult;
 
 class EmoteInfo implements Stringable {
     public function __construct(
-        private string $id,
-        private int $order,
-        private int $rank,
-        private string $url,
+        public private(set) string $id,
+        public private(set) int $order,
+        public private(set) int $minRank,
+        public private(set) string $url,
     ) {}
 
     public static function fromResult(DbResult $result): EmoteInfo {
         return new EmoteInfo(
             id: $result->getString(0),
             order: $result->getInteger(1),
-            rank: $result->getInteger(2),
+            minRank: $result->getInteger(2),
             url: $result->getString(3),
         );
     }
 
-    public function getId(): string {
-        return $this->id;
-    }
-
-    public function getOrder(): int {
-        return $this->order;
-    }
-
-    public function getMinRank(): int {
-        return $this->rank;
-    }
-
-    public function getUrl(): string {
-        return $this->url;
-    }
-
     public function __toString(): string {
         return $this->url;
     }
diff --git a/src/Emoticons/EmoteStringInfo.php b/src/Emoticons/EmoteStringInfo.php
index f1f8b356..47157b34 100644
--- a/src/Emoticons/EmoteStringInfo.php
+++ b/src/Emoticons/EmoteStringInfo.php
@@ -6,9 +6,9 @@ use Index\Db\DbResult;
 
 class EmoteStringInfo implements Stringable {
     public function __construct(
-        private string $emoteId,
-        private int $order,
-        private string $string,
+        public private(set) string $emoteId,
+        public private(set) int $order,
+        public private(set) string $string,
     ) {}
 
     public static function fromResult(DbResult $result): EmoteStringInfo {
@@ -19,18 +19,6 @@ class EmoteStringInfo implements Stringable {
         );
     }
 
-    public function getEmoteId(): string {
-        return $this->emoteId;
-    }
-
-    public function getOrder(): int {
-        return $this->order;
-    }
-
-    public function getString(): string {
-        return $this->string;
-    }
-
     public function __toString(): string {
         return $this->string;
     }
diff --git a/src/Emoticons/Emotes.php b/src/Emoticons/Emotes.php
index 26f5df9f..4156d744 100644
--- a/src/Emoticons/Emotes.php
+++ b/src/Emoticons/Emotes.php
@@ -12,11 +12,11 @@ class Emotes {
         'rank' => 'emote_hierarchy',
     ];
 
-    private DbConnection $dbConn;
     private DbStatementCache $cache;
 
-    public function __construct(DbConnection $dbConn) {
-        $this->dbConn = $dbConn;
+    public function __construct(
+        private DbConnection $dbConn
+    ) {
         $this->cache = new DbStatementCache($dbConn);
     }
 
@@ -108,7 +108,7 @@ class Emotes {
 
     public function deleteEmote(EmoteInfo|string $infoOrId): void {
         if($infoOrId instanceof EmoteInfo)
-            $infoOrId = $infoOrId->getId();
+            $infoOrId = $infoOrId->id;
 
         $stmt = $this->cache->get('DELETE FROM msz_emoticons WHERE emote_id = ?');
         $stmt->addParameter(1, $infoOrId);
@@ -128,7 +128,7 @@ class Emotes {
         }
 
         if($infoOrId instanceof EmoteInfo)
-            $infoOrId = $infoOrId->getId();
+            $infoOrId = $infoOrId->id;
 
         $stmt = $this->cache->get('UPDATE msz_emoticons SET emote_order = COALESCE(?, emote_order), emote_hierarchy = COALESCE(?, emote_hierarchy), emote_url = COALESCE(?, emote_url) WHERE emote_id = ?');
         $stmt->addParameter(1, $order);
@@ -141,7 +141,7 @@ class Emotes {
     public function updateEmoteOrderOffset(EmoteInfo|string $infoOrId, int $offset): void {
         if($offset === 0) return;
         if($infoOrId instanceof EmoteInfo)
-            $infoOrId = $infoOrId->getId();
+            $infoOrId = $infoOrId->id;
 
         $stmt = $this->cache->get('UPDATE msz_emoticons SET emote_order = emote_order + ? WHERE emote_id = ?');
         $stmt->addParameter(1, $offset);
@@ -151,7 +151,7 @@ class Emotes {
 
     public function getEmoteStrings(EmoteInfo|string $infoOrId): iterable {
         if($infoOrId instanceof EmoteInfo)
-            $infoOrId = $infoOrId->getId();
+            $infoOrId = $infoOrId->id;
 
         $stmt = $this->cache->get('SELECT emote_id, emote_string_order, emote_string FROM msz_emoticons_strings WHERE emote_id = ? ORDER BY emote_string_order');
         $stmt->addParameter(1, $infoOrId);
@@ -195,7 +195,7 @@ class Emotes {
             throw new InvalidArgumentException('$string is not correctly formatted: ' . $check);
 
         if($infoOrId instanceof EmoteInfo)
-            $infoOrId = $infoOrId->getId();
+            $infoOrId = $infoOrId->id;
 
         $stmt = $this->cache->get('INSERT INTO msz_emoticons_strings (emote_id, emote_string, emote_string_order) SELECT ? AS target_emote_id, ?, COALESCE(?, (SELECT MAX(emote_string_order) + 1 FROM msz_emoticons_strings WHERE emote_id = target_emote_id), 1)');
         $stmt->addParameter(1, $infoOrId);
@@ -206,7 +206,7 @@ class Emotes {
 
     public function removeEmoteString(EmoteStringInfo|string $infoOrString): void {
         if($infoOrString instanceof EmoteStringInfo)
-            $infoOrString = $infoOrString->getString();
+            $infoOrString = $infoOrString->string;
 
         $stmt = $this->cache->get('DELETE FROM msz_emoticons_strings WHERE emote_string = ?');
         $stmt->addParameter(1, $infoOrString);
diff --git a/src/Emoticons/EmotesRpcHandler.php b/src/Emoticons/EmotesRpcHandler.php
index 28506fc7..5391183a 100644
--- a/src/Emoticons/EmotesRpcHandler.php
+++ b/src/Emoticons/EmotesRpcHandler.php
@@ -17,20 +17,20 @@ final class EmotesRpcHandler implements RpcHandler {
             $this->emotes->getEmotes(orderBy: 'order'),
             function($emote) use ($includeId, $includeOrder) {
                 $info = [
-                    'url' => $emote->getUrl(),
+                    'url' => $emote->url,
                     'strings' => XArray::select(
                         $this->emotes->getEmoteStrings($emote),
-                        fn($string) => $string->getString()
+                        fn($string) => $string->string
                     ),
                 ];
 
                 if($includeId)
-                    $info['id'] = $emote->getId();
+                    $info['id'] = $emote->id;
                 if($includeOrder)
-                    $info['order'] = $emote->getOrder();
+                    $info['order'] = $emote->order;
 
-                $rank = $emote->getMinRank();
-                if($rank != 0)
+                $rank = $emote->minRank;
+                if($rank !== 0)
                     $info['min_rank'] = $rank;
 
                 return $info;
diff --git a/src/Forum/ForumCategories.php b/src/Forum/ForumCategories.php
index 95d304f0..8d48a9e5 100644
--- a/src/Forum/ForumCategories.php
+++ b/src/Forum/ForumCategories.php
@@ -28,19 +28,19 @@ class ForumCategories {
         $tree = [];
         $predicate = $parentInfo
             ? fn($catInfo) => $catInfo->isDirectChildOf($parentInfo)
-            : fn($catInfo) => !$catInfo->hasParent();
+            : fn($catInfo) => !$catInfo->hasParent;
 
         foreach($catInfos as $catInfo) {
             if(!$predicate($catInfo))
                 continue;
 
-            $tree[$catInfo->getId()] = $item = new stdClass;
+            $tree[$catInfo->id] = $item = new stdClass;
             $item->info = $catInfo;
-            $item->colour = $catInfo->hasColour() ? $catInfo->getColour() : $colour;
+            $item->colour = $catInfo->hasColour ? $catInfo->colour : $colour;
             $item->children = self::convertCategoryListToTree($catInfos, $catInfo, $item->colour);
             $item->childIds = [];
             foreach($item->children as $child) {
-                $item->childIds[] = $child->info->getId();
+                $item->childIds[] = $child->info->id;
                 $item->childIds += $child->childIds;
             }
         }
@@ -54,7 +54,7 @@ class ForumCategories {
         ?bool $hidden = null
     ): int {
         if($parentInfo instanceof ForumCategoryInfo)
-            $parentInfo = $parentInfo->getId();
+            $parentInfo = $parentInfo->id;
 
         $isRootParent = false;
         $hasParentInfo = $parentInfo !== false;
@@ -142,7 +142,7 @@ class ForumCategories {
         $stmt = $this->cache->get($query);
         if($hasParentInfo && !$isRootParent) {
             if($parentInfo instanceof ForumCategoryInfo)
-                $stmt->addParameter(++$args, $parentInfo->getId());
+                $stmt->addParameter(++$args, $parentInfo->id);
             else
                 $stmt->addParameter(++$args, $parentInfo);
         }
@@ -187,7 +187,7 @@ class ForumCategories {
         if($hasTopicInfo) {
             if($topicInfo instanceof ForumTopicInfo) {
                 $query .= ' WHERE forum_id = ?';
-                $value = $topicInfo->getCategoryId();
+                $value = $topicInfo->categoryId;
             } else {
                 $query .= ' WHERE forum_id = (SELECT forum_id FROM msz_forum_topics WHERE topic_id = ?)';
                 $value = $topicInfo;
@@ -196,7 +196,7 @@ class ForumCategories {
         if($hasPostInfo) {
             if($postInfo instanceof ForumPostInfo) {
                 $query .= ' WHERE forum_id = ?';
-                $value = $postInfo->getCategoryId();
+                $value = $postInfo->categoryId;
             } else {
                 $query .= ' WHERE forum_id = (SELECT forum_id FROM msz_forum_posts WHERE post_id = ?)';
                 $value = $postInfo;
@@ -218,17 +218,17 @@ class ForumCategories {
         ForumCategoryInfo|string $categoryInfo
     ): void {
         if($categoryInfo instanceof ForumCategoryInfo)
-            $categoryInfo = $categoryInfo->getId();
+            $categoryInfo = $categoryInfo->id;
     }
 
     public function deleteCategory(ForumCategoryInfo|string $categoryInfo): void {
         if($categoryInfo instanceof ForumCategoryInfo)
-            $categoryInfo = $categoryInfo->getId();
+            $categoryInfo = $categoryInfo->id;
     }
 
     public function incrementCategoryClicks(ForumCategoryInfo|string $categoryInfo): void {
         if($categoryInfo instanceof ForumCategoryInfo)
-            $categoryInfo = $categoryInfo->getId();
+            $categoryInfo = $categoryInfo->id;
 
         // previous implementation also WHERE'd for forum_type = link but i don't think there's any other way to get here anyhow
         $stmt = $this->cache->get('UPDATE msz_forum_categories SET forum_link_clicks = forum_link_clicks + 1 WHERE forum_id = ? AND forum_link_clicks IS NOT NULL');
@@ -238,7 +238,7 @@ class ForumCategories {
 
     public function incrementCategoryTopics(ForumCategoryInfo|string $categoryInfo): void {
         if($categoryInfo instanceof ForumCategoryInfo)
-            $categoryInfo = $categoryInfo->getId();
+            $categoryInfo = $categoryInfo->id;
 
         $stmt = $this->cache->get('UPDATE msz_forum_categories SET forum_count_topics = forum_count_topics + 1 WHERE forum_id = ?');
         $stmt->addParameter(1, $categoryInfo);
@@ -247,7 +247,7 @@ class ForumCategories {
 
     public function incrementCategoryPosts(ForumCategoryInfo|string $categoryInfo): void {
         if($categoryInfo instanceof ForumCategoryInfo)
-            $categoryInfo = $categoryInfo->getId();
+            $categoryInfo = $categoryInfo->id;
 
         $stmt = $this->cache->get('UPDATE msz_forum_categories SET forum_count_posts = forum_count_posts + 1 WHERE forum_id = ?');
         $stmt->addParameter(1, $categoryInfo);
@@ -258,9 +258,9 @@ class ForumCategories {
         ForumCategoryInfo|ForumTopicInfo|ForumPostInfo|string $categoryInfo
     ): iterable {
         if($categoryInfo instanceof ForumCategoryInfo)
-            $categoryInfo = $categoryInfo->getId();
+            $categoryInfo = $categoryInfo->id;
         elseif($categoryInfo instanceof ForumTopicInfo || $categoryInfo instanceof ForumPostInfo)
-            $categoryInfo = $categoryInfo->getCategoryId();
+            $categoryInfo = $categoryInfo->categoryId;
 
         $query = 'WITH RECURSIVE msz_cte_ancestry AS ('
             . 'SELECT forum_id, forum_order, forum_parent, forum_name, forum_type, forum_description, forum_icon, forum_colour, forum_link, forum_link_clicks, UNIX_TIMESTAMP(forum_created), forum_archived, forum_hidden, forum_count_topics, forum_count_posts FROM msz_forum_categories WHERE forum_id = ?'
@@ -282,7 +282,7 @@ class ForumCategories {
         bool $asTree = false
     ): iterable {
         if($parentInfo instanceof ForumCategoryInfo)
-            $parentInfo = $parentInfo->getId();
+            $parentInfo = $parentInfo->id;
 
         $hasHidden = $hidden !== null;
 
@@ -336,7 +336,7 @@ class ForumCategories {
         $stmt->addParameter(++$args, $userInfo);
         foreach($categoryInfos as $categoryInfo) {
             if($categoryInfo instanceof ForumCategoryInfo)
-                $stmt->addParameter(++$args, $categoryInfo->getId());
+                $stmt->addParameter(++$args, $categoryInfo->id);
             elseif(is_string($categoryInfo) || is_int($categoryInfo))
                 $stmt->addParameter(++$args, $categoryInfo);
             else
@@ -357,8 +357,8 @@ class ForumCategories {
 
         if($userInfo instanceof UserInfo)
             $userInfo = $userInfo->getId();
-        if($categoryInfo instanceof $categoryInfo)
-            $categoryInfo = $categoryInfo->getId();
+        if($categoryInfo instanceof ForumCategoryInfo)
+            $categoryInfo = $categoryInfo->id;
 
         $stmt = $this->cache->get('REPLACE INTO msz_forum_topics_track (user_id, topic_id, forum_id, track_last_read) SELECT ?, topic_id, forum_id, NOW() FROM msz_forum_topics WHERE forum_id = ? AND topic_bumped >= NOW() - INTERVAL 1 MONTH');
         $stmt->addParameter(1, $userInfo);
@@ -370,9 +370,9 @@ class ForumCategories {
         ForumCategoryInfo|ForumTopicInfo|ForumPostInfo|string $categoryInfo
     ): Colour {
         if($categoryInfo instanceof ForumCategoryInfo)
-            $categoryInfo = $categoryInfo->getId();
+            $categoryInfo = $categoryInfo->id;
         elseif($categoryInfo instanceof ForumTopicInfo || $categoryInfo instanceof ForumPostInfo)
-            $categoryInfo = $categoryInfo->getCategoryId();
+            $categoryInfo = $categoryInfo->categoryId;
 
         $query = 'WITH RECURSIVE msz_cte_colours AS ('
             . 'SELECT forum_id, forum_parent, forum_colour FROM msz_forum_categories WHERE forum_id = ?'
@@ -415,7 +415,7 @@ class ForumCategories {
         $stmt->addParameter(++$args, $userInfo);
         foreach($exceptCategoryInfos as $categoryInfo) {
             if($categoryInfo instanceof ForumCategoryInfo)
-                $stmt->addParameter(++$args, $categoryInfo->getId());
+                $stmt->addParameter(++$args, $categoryInfo->id);
             elseif(is_string($categoryInfo) || is_int($categoryInfo))
                 $stmt->addParameter(++$args, (string)$categoryInfo);
             else
@@ -423,7 +423,7 @@ class ForumCategories {
         }
         foreach($exceptTopicInfos as $topicInfo) {
             if($topicInfo instanceof ForumTopicInfo)
-                $stmt->addParameter(++$args, $topicInfo->getId());
+                $stmt->addParameter(++$args, $topicInfo->id);
             elseif(is_string($topicInfo) || is_int($topicInfo))
                 $stmt->addParameter(++$args, (string)$topicInfo);
             else
@@ -447,7 +447,7 @@ class ForumCategories {
         bool $updateCounters = true
     ): object {
         if($categoryInfo instanceof ForumCategoryInfo)
-            $categoryInfo = $categoryInfo->getId();
+            $categoryInfo = $categoryInfo->id;
         elseif($categoryInfo === null)
             $categoryInfo = '0';
 
diff --git a/src/Forum/ForumCategoryInfo.php b/src/Forum/ForumCategoryInfo.php
index 822d6840..35237992 100644
--- a/src/Forum/ForumCategoryInfo.php
+++ b/src/Forum/ForumCategoryInfo.php
@@ -27,21 +27,21 @@ class ForumCategoryInfo {
     ];
 
     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 private(set) string $id,
+        public private(set) int $order,
+        public private(set) ?string $parentId,
+        public private(set) string $name,
+        public private(set) int $type,
+        public private(set) ?string $description,
+        public private(set) ?string $icon,
+        public private(set) ?int $colourRaw,
+        public private(set) ?string $linkTarget,
+        public private(set) ?int $linkClicks,
+        public private(set) int $createdTime,
+        public private(set) bool $archived,
+        public private(set) bool $hidden,
+        public private(set) int $topicsCount,
+        public private(set) int $postsCount,
     ) {}
 
     public static function fromResult(DbResult $result): ForumCategoryInfo {
@@ -51,12 +51,12 @@ class ForumCategoryInfo {
             parentId: $result->getStringOrNull(2),
             name: $result->getString(3),
             type: $result->getInteger(4),
-            desc: $result->getStringOrNull(5),
+            description: $result->getStringOrNull(5),
             icon: $result->getStringOrNull(6),
-            colour: $result->getIntegerOrNull(7),
-            link: $result->getStringOrNull(8),
-            clicks: $result->getIntegerOrNull(9),
-            created: $result->getInteger(10),
+            colourRaw: $result->getIntegerOrNull(7),
+            linkTarget: $result->getStringOrNull(8),
+            linkClicks: $result->getIntegerOrNull(9),
+            createdTime: $result->getInteger(10),
             archived: $result->getBoolean(11),
             hidden: $result->getBoolean(12),
             topicsCount: $result->getInteger(13),
@@ -64,135 +64,62 @@ class ForumCategoryInfo {
         );
     }
 
-    public function getId(): string {
-        return $this->id;
-    }
-
-    public function getOrder(): int {
-        return $this->order;
-    }
-
-    public function hasParent(): bool {
-        return $this->parentId !== null && $this->parentId !== '0';
-    }
-
-    public function getParentId(): ?string {
-        return $this->parentId;
+    public bool $hasParent {
+        get => $this->parentId !== null && $this->parentId !== '0';
     }
 
     public function isDirectChildOf(ForumCategoryInfo|string $parentInfo): bool {
         if($parentInfo instanceof ForumCategoryInfo)
-            $parentInfo = $parentInfo->getId();
-        return $this->hasParent() && $this->getParentId() === $parentInfo;
+            $parentInfo = $parentInfo->id;
+
+        return $this->hasParent && $this->parentId === $parentInfo;
     }
 
-    public function getName(): string {
-        return $this->name;
+    public bool $isDiscussion {
+        get => $this->type === self::TYPE_DISCUSSION;
     }
 
-    public function getType(): int {
-        return $this->type;
+    public bool $isListing {
+        get => $this->type === self::TYPE_LISTING;
     }
 
-    public function isDiscussion(): bool {
-        return $this->type === self::TYPE_DISCUSSION;
+    public bool $isLink {
+        get => $this->type === self::TYPE_LINK;
     }
 
-    public function isListing(): bool {
-        return $this->type === self::TYPE_LISTING;
+    public bool $mayHaveChildren {
+        get => in_array($this->type, self::MAY_HAVE_CHILDREN);
     }
 
-    public function isLink(): bool {
-        return $this->type === self::TYPE_LINK;
+    public bool $mayHaveTopics {
+        get => in_array($this->type, self::MAY_HAVE_TOPICS);
     }
 
-    public function mayHaveChildren(): bool {
-        return in_array($this->type, self::MAY_HAVE_CHILDREN);
+    public string $iconForDisplay {
+        get {
+            if(!empty($this->icon))
+                return $this->icon;
+
+            if($this->archived)
+                return 'fas fa-archive fa-fw';
+
+            return match($this->type) {
+                self::TYPE_LISTING => 'fas fa-folder fa-fw',
+                self::TYPE_LINK => 'fas fa-link fa-fw',
+                default => 'fas fa-comments fa-fw',
+            };
+        }
     }
 
-    public function mayHaveTopics(): bool {
-        return in_array($this->type, self::MAY_HAVE_TOPICS);
+    public bool $hasColour {
+        get => $this->colourRaw !== null && ($this->colourRaw & 0x40000000) === 0;
     }
 
-    public function hasDescription(): bool {
-        return $this->desc !== null && $this->desc !== '';
+    public Colour $colour {
+        get => $this->colourRaw === null ? Colour::none() : Colour::fromMisuzu($this->colourRaw);
     }
 
-    public function getDescription(): ?string {
-        return $this->desc;
-    }
-
-    public function hasIcon(): bool {
-        return $this->icon !== null && $this->icon !== '';
-    }
-
-    public function getIcon(): ?string {
-        return $this->icon;
-    }
-
-    public function getIconForDisplay(): string {
-        if($this->hasIcon())
-            return $this->getIcon();
-
-        if($this->isArchived())
-            return 'fas fa-archive fa-fw';
-
-        return match($this->type) {
-            self::TYPE_LISTING => 'fas fa-folder fa-fw',
-            self::TYPE_LINK => 'fas fa-link fa-fw',
-            default => 'fas fa-comments fa-fw',
-        };
-    }
-
-    public function hasColour(): bool {
-        return $this->colour !== null && ($this->colour & 0x40000000) === 0;
-    }
-
-    public function getColourRaw(): ?int {
-        return $this->colour;
-    }
-
-    public function getColour(): Colour {
-        return $this->colour === null ? Colour::none() : Colour::fromMisuzu($this->colour);
-    }
-
-    public function hasLinkTarget(): bool {
-        return $this->link !== null && $this->link !== '';
-    }
-
-    public function getLinkTarget(): ?string {
-        return $this->link;
-    }
-
-    public function hasLinkClicks(): bool {
-        return $this->clicks !== null;
-    }
-
-    public function getLinkClicks(): ?int {
-        return $this->clicks;
-    }
-
-    public function getCreatedTime(): int {
-        return $this->created;
-    }
-
-    public function getCreatedAt(): CarbonImmutable {
-        return CarbonImmutable::createFromTimestampUTC($this->created);
-    }
-
-    public function isArchived(): bool {
-        return $this->archived;
-    }
-
-    public function isHidden(): bool {
-        return $this->hidden;
-    }
-
-    public function getTopicsCount(): int {
-        return $this->topicsCount;
-    }
-
-    public function getPostsCount(): int {
-        return $this->postsCount;
+    public CarbonImmutable $createdAt {
+        get => CarbonImmutable::createFromTimestampUTC($this->createdTime);
     }
 }
diff --git a/src/Forum/ForumContext.php b/src/Forum/ForumContext.php
index e361e629..a9e024e3 100644
--- a/src/Forum/ForumContext.php
+++ b/src/Forum/ForumContext.php
@@ -6,10 +6,10 @@ use Index\Db\DbConnection;
 use Misuzu\Users\UserInfo;
 
 class ForumContext {
-    private ForumCategories $categories;
-    private ForumTopics $topics;
-    private ForumTopicRedirects $topicRedirects;
-    private ForumPosts $posts;
+    public private(set) ForumCategories $categories;
+    public private(set) ForumTopics $topics;
+    public private(set) ForumTopicRedirects $topicRedirects;
+    public private(set) ForumPosts $posts;
 
     private array $totalUserTopics = [];
     private array $totalUserPosts = [];
@@ -21,22 +21,6 @@ class ForumContext {
         $this->posts = new ForumPosts($dbConn);
     }
 
-    public function getCategories(): ForumCategories {
-        return $this->categories;
-    }
-
-    public function getTopics(): ForumTopics {
-        return $this->topics;
-    }
-
-    public function getTopicRedirects(): ForumTopicRedirects {
-        return $this->topicRedirects;
-    }
-
-    public function getPosts(): ForumPosts {
-        return $this->posts;
-    }
-
     // should be replaced by a static counter
     public function countTotalUserTopics(UserInfo|string|null $userInfo): int {
         if($userInfo === null)
diff --git a/src/Forum/ForumPostInfo.php b/src/Forum/ForumPostInfo.php
index 1f1cca85..33e72b8d 100644
--- a/src/Forum/ForumPostInfo.php
+++ b/src/Forum/ForumPostInfo.php
@@ -8,17 +8,17 @@ use Index\Db\DbResult;
 
 class ForumPostInfo {
     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 private(set) string $id,
+        public private(set) string $topicId,
+        public private(set) string $categoryId,
+        public private(set) ?string $userId,
+        public private(set) string $remoteAddress,
+        public private(set) string $body,
+        public private(set) int $parser,
+        public private(set) bool $shouldDisplaySignature,
+        public private(set) int $createdTime,
+        public private(set) ?int $editedTime,
+        public private(set) ?int $deletedTime,
     ) {}
 
     public static function fromResult(DbResult $result): ForumPostInfo {
@@ -27,111 +27,65 @@ class ForumPostInfo {
             topicId: $result->getString(1),
             categoryId: $result->getString(2),
             userId: $result->getStringOrNull(3),
-            remoteAddr: $result->getString(4),
+            remoteAddress: $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),
+            shouldDisplaySignature: $result->getBoolean(7),
+            createdTime: $result->getInteger(8),
+            editedTime: $result->getIntegerOrNull(9),
+            deletedTime: $result->getIntegerOrNull(10),
         );
     }
 
-    public function getId(): string {
-        return $this->id;
+    public bool $isBodyPlain {
+        get => $this->parser === Parser::PLAIN;
     }
 
-    public function getTopicId(): string {
-        return $this->topicId;
+    public bool $isBodyBBCode {
+        get => $this->parser === Parser::BBCODE;
     }
 
-    public function getCategoryId(): string {
-        return $this->categoryId;
+    public bool $isBodyMarkdown {
+        get => $this->parser === Parser::MARKDOWN;
     }
 
-    public function hasUserId(): bool {
-        return $this->userId !== null;
-    }
-
-    public function getUserId(): ?string {
-        return $this->userId;
-    }
-
-    public function getRemoteAddress(): string {
-        return $this->remoteAddr;
-    }
-
-    public function getBody(): string {
-        return $this->body;
-    }
-
-    public function getParser(): int {
-        return $this->parser;
-    }
-
-    public function isBodyPlain(): bool {
-        return $this->parser === Parser::PLAIN;
-    }
-
-    public function isBodyBBCode(): bool {
-        return $this->parser === Parser::BBCODE;
-    }
-
-    public function isBodyMarkdown(): bool {
-        return $this->parser === Parser::MARKDOWN;
-    }
-
-    public function shouldDisplaySignature(): bool {
-        return $this->displaySignature;
-    }
-
-    public function getCreatedTime(): int {
-        return $this->created;
-    }
-
-    public function getCreatedAt(): CarbonImmutable {
-        return CarbonImmutable::createFromTimestampUTC($this->created);
+    public CarbonImmutable $createdAt {
+        get => CarbonImmutable::createFromTimestampUTC($this->createdTime);
     }
 
     private static ?CarbonImmutable $markAsEditedThreshold = null;
 
-    public function shouldMarkAsEdited(): bool {
-        if(self::$markAsEditedThreshold === null)
-            self::$markAsEditedThreshold = new CarbonImmutable('-5 minutes');
+    public bool $shouldMarkAsEdited {
+        get {
+            self::$markAsEditedThreshold ??= new CarbonImmutable('-5 minutes');
 
-        return XDateTime::compare($this->getCreatedAt(), self::$markAsEditedThreshold) < 0;
+            return XDateTime::compare($this->createdAt, self::$markAsEditedThreshold) < 0;
+        }
     }
 
-    public function isEdited(): bool {
-        return $this->edited !== null;
+    public bool $edited {
+        get => $this->editedTime !== null;
     }
 
-    public function getEditedTime(): ?int {
-        return $this->edited;
-    }
-
-    public function getEditedAt(): ?CarbonImmutable {
-        return $this->edited === null ? null : CarbonImmutable::createFromTimestampUTC($this->edited);
+    public ?CarbonImmutable $editedAt {
+        get => $this->editedTime === null ? null : CarbonImmutable::createFromTimestampUTC($this->editedTime);
     }
 
     private static ?CarbonImmutable $canBeDeletedThreshold = null;
 
-    public function canBeDeleted(): bool {
-        if(self::$canBeDeletedThreshold === null)
-            self::$canBeDeletedThreshold = new CarbonImmutable('-1 week');
+    public bool $canBeDeleted {
+        get {
+            self::$canBeDeletedThreshold ??= new CarbonImmutable('-1 week');
 
-        return XDateTime::compare($this->getCreatedAt(), self::$canBeDeletedThreshold) >= 0;
+            return XDateTime::compare($this->createdAt, self::$canBeDeletedThreshold) >= 0;
+        }
     }
 
-    public function isDeleted(): bool {
-        return $this->deleted !== null;
+    public bool $deleted {
+        get => $this->deletedTime !== null;
     }
 
-    public function getDeletedTime(): ?int {
-        return $this->deleted;
-    }
-
-    public function getDeletedAt(): ?CarbonImmutable {
-        return $this->deleted === null ? null : CarbonImmutable::createFromTimestampUTC($this->deleted);
+    public ?CarbonImmutable $deletedAt {
+        get => $this->deletedTime === null ? null : CarbonImmutable::createFromTimestampUTC($this->deletedTime);
     }
 }
diff --git a/src/Forum/ForumPosts.php b/src/Forum/ForumPosts.php
index 97970756..7388b342 100644
--- a/src/Forum/ForumPosts.php
+++ b/src/Forum/ForumPosts.php
@@ -10,11 +10,11 @@ use Misuzu\Pagination;
 use Misuzu\Users\UserInfo;
 
 class ForumPosts {
-    private DbConnection $dbConn;
     private DbStatementCache $cache;
 
-    public function __construct(DbConnection $dbConn) {
-        $this->dbConn = $dbConn;
+    public function __construct(
+        private DbConnection $dbConn
+    ) {
         $this->cache = new DbStatementCache($dbConn);
     }
 
@@ -26,13 +26,13 @@ class ForumPosts {
         ?bool $deleted = null
     ): int {
         if($categoryInfo instanceof ForumCategoryInfo)
-            $categoryInfo = $categoryInfo->getId();
+            $categoryInfo = $categoryInfo->id;
         if($topicInfo instanceof ForumTopicInfo)
-            $topicInfo = $topicInfo->getId();
+            $topicInfo = $topicInfo->id;
         if($userInfo instanceof UserInfo)
             $userInfo = $userInfo->getId();
         if($upToPostInfo instanceof ForumPostInfo)
-            $upToPostInfo = $upToPostInfo->getId();
+            $upToPostInfo = $upToPostInfo->id;
 
         $hasCategoryInfo = $categoryInfo !== null;
         $hasTopicInfo = $topicInfo !== null;
@@ -109,15 +109,15 @@ class ForumPosts {
         }
 
         if($categoryInfo instanceof ForumCategoryInfo)
-            $categoryInfo = $categoryInfo->getId();
+            $categoryInfo = $categoryInfo->id;
         if($topicInfo instanceof ForumTopicInfo)
-            $topicInfo = $topicInfo->getId();
+            $topicInfo = $topicInfo->id;
         if($userInfo instanceof UserInfo)
             $userInfo = $userInfo->getId();
         if($upToPostInfo instanceof ForumPostInfo)
-            $upToPostInfo = $upToPostInfo->getId();
+            $upToPostInfo = $upToPostInfo->id;
         if($afterPostInfo instanceof ForumPostInfo)
-            $afterPostInfo = $afterPostInfo->getId();
+            $afterPostInfo = $afterPostInfo->id;
 
         $hasCategoryInfo = $categoryInfo !== null;
         $hasTopicInfo = $topicInfo !== null;
@@ -164,7 +164,7 @@ class ForumPosts {
         if($hasCategoryInfo) {
             if(is_array($categoryInfo)) {
                 foreach($categoryInfo as $categoryInfoEntry)
-                    $stmt->addParameter(++$args, $categoryInfoEntry instanceof ForumCategoryInfo ? $categoryInfoEntry->getId() : (string)$categoryInfoEntry);
+                    $stmt->addParameter(++$args, $categoryInfoEntry instanceof ForumCategoryInfo ? $categoryInfoEntry->id : (string)$categoryInfoEntry);
             } else
                 $stmt->addParameter(++$args, $categoryInfo);
         }
@@ -217,7 +217,7 @@ class ForumPosts {
             $query .= sprintf(' ORDER BY post_id %s', $getLast ? 'DESC' : 'ASC');
         } elseif($hasTopicInfo) {
             if($topicInfo instanceof ForumTopicInfo)
-                $topicInfo = $topicInfo->getId();
+                $topicInfo = $topicInfo->id;
 
             $query .= sprintf(' WHERE post_id = (SELECT %s(post_id) FROM msz_forum_posts WHERE topic_id = ?', $getLast ? 'MAX' : 'MIN');
             if($hasDeleted)
@@ -240,7 +240,7 @@ class ForumPosts {
 
             foreach($categoryInfos as $categoryInfo) {
                 if($categoryInfo instanceof ForumCategoryInfo)
-                    $values[] = $categoryInfo->getId();
+                    $values[] = $categoryInfo->id;
                 elseif(is_string($categoryInfo) || is_int($categoryInfo))
                     $values[] = (string)$categoryInfo;
                 else
@@ -271,11 +271,11 @@ class ForumPosts {
         ForumCategoryInfo|string|null $categoryInfo = null
     ): ForumPostInfo {
         if($categoryInfo instanceof ForumCategoryInfo)
-            $categoryInfo = $categoryInfo->getId();
+            $categoryInfo = $categoryInfo->id;
 
         if($topicInfo instanceof ForumTopicInfo) {
-            $categoryInfo ??= $topicInfo->getCategoryId();
-            $topicInfo = $topicInfo->getId();
+            $categoryInfo ??= $topicInfo->categoryId;
+            $topicInfo = $topicInfo->id;
         } elseif($categoryInfo === null)
             throw new InvalidArgumentException('$categoryInfo may only be null if $topicInfo is an instance of ForumTopicInfo.');
 
@@ -304,7 +304,7 @@ class ForumPosts {
         bool $bumpEdited = true
     ): void {
         if($postInfo instanceof ForumPostInfo)
-            $postInfo = $postInfo->getId();
+            $postInfo = $postInfo->id;
 
         $fields = [];
         $values = [];
@@ -345,7 +345,7 @@ class ForumPosts {
 
     public function deletePost(ForumPostInfo|string $postInfo): void {
         if($postInfo instanceof ForumPostInfo)
-            $postInfo = $postInfo->getId();
+            $postInfo = $postInfo->id;
 
         $stmt = $this->cache->get('UPDATE msz_forum_posts SET post_deleted = COALESCE(post_deleted, NOW()) WHERE post_id = ?');
         $stmt->addParameter(1, $postInfo);
@@ -354,7 +354,7 @@ class ForumPosts {
 
     public function restorePost(ForumPostInfo|string $postInfo): void {
         if($postInfo instanceof ForumPostInfo)
-            $postInfo = $postInfo->getId();
+            $postInfo = $postInfo->id;
 
         $stmt = $this->cache->get('UPDATE msz_forum_posts SET post_deleted = NULL WHERE post_id = ?');
         $stmt->addParameter(1, $postInfo);
@@ -363,7 +363,7 @@ class ForumPosts {
 
     public function nukePost(ForumPostInfo|string $postInfo): void {
         if($postInfo instanceof ForumPostInfo)
-            $postInfo = $postInfo->getId();
+            $postInfo = $postInfo->id;
 
         $stmt = $this->cache->get('DELETE FROM msz_forum_posts WHERE post_id = ?');
         $stmt->addParameter(1, $postInfo);
@@ -418,9 +418,9 @@ class ForumPosts {
         $args = 0;
         $stmt = $this->cache->get($query);
         foreach($exceptCategoryInfos as $exceptCategoryInfo)
-            $stmt->addParameter(++$args, $exceptCategoryInfo instanceof ForumCategoryInfo ? $exceptCategoryInfo->getId() : $exceptCategoryInfo);
+            $stmt->addParameter(++$args, $exceptCategoryInfo instanceof ForumCategoryInfo ? $exceptCategoryInfo->id : $exceptCategoryInfo);
         foreach($exceptTopicInfos as $exceptTopicInfo)
-            $stmt->addParameter(++$args, $exceptTopicInfo instanceof ForumTopicInfo ? $exceptTopicInfo->getId() : $exceptTopicInfo);
+            $stmt->addParameter(++$args, $exceptTopicInfo instanceof ForumTopicInfo ? $exceptTopicInfo->id : $exceptTopicInfo);
         $stmt->execute();
 
         $result = $stmt->getResult();
diff --git a/src/Forum/ForumTopicInfo.php b/src/Forum/ForumTopicInfo.php
index 2278dcbe..37c0b4af 100644
--- a/src/Forum/ForumTopicInfo.php
+++ b/src/Forum/ForumTopicInfo.php
@@ -19,18 +19,18 @@ class ForumTopicInfo {
     ];
 
     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 private(set) string $id,
+        public private(set) string $categoryId,
+        public private(set) ?string $userId,
+        public private(set) int $type,
+        public private(set) string $title,
+        public private(set) int $viewsCount,
+        public private(set) int $createdTime,
+        public private(set) int $bumpedTime,
+        public private(set) ?int $deletedTime,
+        public private(set) ?int $lockedTime,
+        public private(set) int $postsCount,
+        public private(set) int $deletedPostsCount,
     ) {}
 
     public static function fromResult(DbResult $result): ForumTopicInfo {
@@ -41,37 +41,17 @@ class ForumTopicInfo {
             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),
+            createdTime: $result->getInteger(6),
+            bumpedTime: $result->getInteger(7),
+            deletedTime: $result->getIntegerOrNull(8),
+            lockedTime: $result->getIntegerOrNull(9),
             postsCount: $result->getInteger(10),
             deletedPostsCount: $result->getInteger(11),
         );
     }
 
-    public function getId(): string {
-        return $this->id;
-    }
-
-    public function getCategoryId(): string {
-        return $this->categoryId;
-    }
-
-    public function hasUserId(): bool {
-        return $this->userId !== null;
-    }
-
-    public function getUserId(): ?string {
-        return $this->userId;
-    }
-
-    public function getType(): int {
-        return $this->type;
-    }
-
-    public function getTypeString(): string {
-        return match($this->type) {
+    public string $typeString {
+        get => match($this->type) {
             self::TYPE_GLOBAL => 'global',
             self::TYPE_ANNOUNCE => 'announce',
             self::TYPE_STICKY => 'sticky',
@@ -79,106 +59,75 @@ class ForumTopicInfo {
         };
     }
 
-    public function isDiscussion(): bool {
-        return $this->type === self::TYPE_DISCUSSION;
+    public bool $isDiscussion {
+        get => $this->type === self::TYPE_DISCUSSION;
     }
 
-    public function isSticky(): bool {
-        return $this->type === self::TYPE_STICKY;
+    public bool $isSticky {
+        get => $this->type === self::TYPE_STICKY;
     }
 
-    public function isAnnouncement(): bool {
-        return $this->type === self::TYPE_ANNOUNCE;
+    public bool $isAnnouncement {
+        get => $this->type === self::TYPE_ANNOUNCE;
     }
 
-    public function isGlobalAnnouncement(): bool {
-        return $this->type === self::TYPE_GLOBAL;
+    public bool $isGlobalAnnouncement {
+        get => $this->type === self::TYPE_GLOBAL;
     }
 
-    public function isImportant(): bool {
-        return $this->isSticky()
-            || $this->isAnnouncement()
-            || $this->isGlobalAnnouncement();
+    public bool $isImportant {
+        get => $this->isSticky
+            || $this->isAnnouncement
+            || $this->isGlobalAnnouncement;
     }
 
     public function getIconForDisplay(bool $unread = false): string {
-        if($this->isDeleted())
+        if($this->deleted)
             return 'fas fa-trash-alt fa-fw';
-        if($this->isAnnouncement() || $this->isGlobalAnnouncement())
+        if($this->isAnnouncement || $this->isGlobalAnnouncement)
             return 'fas fa-bullhorn fa-fw';
-        if($this->isSticky())
+        if($this->isSticky)
             return 'fas fa-thumbtack fa-fw';
-        if($this->isLocked())
+        if($this->locked)
             return 'fas fa-lock fa-fw';
         return sprintf('%s fa-comment fa-fw', $unread ? 'fas' : 'far');
     }
 
-    public function getTitle(): string {
-        return $this->title;
+    public int $totalPostsCount {
+        get => $this->postsCount + $this->deletedPostsCount;
     }
 
-    public function getPostsCount(): int {
-        return $this->postsCount;
-    }
-
-    public function getDeletedPostsCount(): int {
-        return $this->deletedPostsCount;
-    }
-
-    public function getTotalPostsCount(): int {
-        return $this->postsCount + $this->deletedPostsCount;
-    }
-
-    public function getViewsCount(): int {
-        return $this->viewsCount;
-    }
-
-    public function getCreatedTime(): int {
-        return $this->created;
-    }
-
-    public function getCreatedAt(): CarbonImmutable {
-        return CarbonImmutable::createFromTimestampUTC($this->created);
+    public CarbonImmutable $createdAt {
+        get => CarbonImmutable::createFromTimestampUTC($this->createdTime);
     }
 
     private static ?CarbonImmutable $lastActiveAt = null;
 
-    public function isActive(): bool {
-        if(self::$lastActiveAt === null)
-            self::$lastActiveAt = new CarbonImmutable('-1 month');
+    public bool $active {
+        get {
+            self::$lastActiveAt ??= new CarbonImmutable('-1 month');
 
-        return XDateTime::compare($this->getBumpedAt(), self::$lastActiveAt) >= 0;
+            return XDateTime::compare($this->bumpedAt, self::$lastActiveAt) >= 0;
+        }
     }
 
-    public function getBumpedTime(): int {
-        return $this->bumped;
+    public CarbonImmutable $bumpedAt {
+        get => CarbonImmutable::createFromTimestampUTC($this->bumpedTime);
     }
 
-    public function getBumpedAt(): CarbonImmutable {
-        return CarbonImmutable::createFromTimestampUTC($this->bumped);
+    public bool $deleted {
+        get => $this->deletedTime !== null;
     }
 
-    public function isDeleted(): bool {
-        return $this->deleted !== null;
+    public ?CarbonImmutable $deletedAt {
+        get => $this->deletedTime === null ? null : CarbonImmutable::createFromTimestampUTC($this->deletedTime);
     }
 
-    public function getDeletedTime(): ?int {
-        return $this->deleted;
+    public bool $locked {
+        get => $this->lockedTime !== null;
     }
 
-    public function getDeletedAt(): ?CarbonImmutable {
-        return $this->deleted === null ? null : CarbonImmutable::createFromTimestampUTC($this->deleted);
-    }
-
-    public function isLocked(): bool {
-        return $this->locked !== null;
-    }
-
-    public function getLockedTime(): ?int {
-        return $this->locked;
-    }
-
-    public function getLockedAt(): ?CarbonImmutable {
-        return $this->locked === null ? null : CarbonImmutable::createFromTimestampUTC($this->locked);
+    public ?CarbonImmutable $lockedAt {
+        get => $this->lockedTime === null ? null : CarbonImmutable::createFromTimestampUTC($this->lockedTime);
     }
 }
diff --git a/src/Forum/ForumTopicRedirectInfo.php b/src/Forum/ForumTopicRedirectInfo.php
index 4e42f1a7..2054340b 100644
--- a/src/Forum/ForumTopicRedirectInfo.php
+++ b/src/Forum/ForumTopicRedirectInfo.php
@@ -6,42 +6,22 @@ use Index\Db\DbResult;
 
 class ForumTopicRedirectInfo {
     public function __construct(
-        private string $topicId,
-        private ?string $userId,
-        private string $link,
-        private int $created,
+        public private(set) string $topicId,
+        public private(set) ?string $userId,
+        public private(set) string $linkTarget,
+        public private(set) int $createdTime,
     ) {}
 
     public static function fromResult(DbResult $result): ForumTopicRedirectInfo {
         return new ForumTopicRedirectInfo(
             topicId: $result->getString(0),
             userId: $result->getStringOrNull(1),
-            link: $result->getString(2),
-            created: $result->getInteger(3),
+            linkTarget: $result->getString(2),
+            createdTime: $result->getInteger(3),
         );
     }
 
-    public function getTopicId(): string {
-        return $this->topicId;
-    }
-
-    public function hasUserId(): bool {
-        return $this->userId !== null;
-    }
-
-    public function getUserId(): ?string {
-        return $this->userId;
-    }
-
-    public function getLinkTarget(): string {
-        return $this->link;
-    }
-
-    public function getCreatedTime(): int {
-        return $this->created;
-    }
-
-    public function getCreatedAt(): CarbonImmutable {
-        return CarbonImmutable::createFromTimestampUTC($this->created);
+    public CarbonImmutable $createdAt {
+        get => CarbonImmutable::createFromTimestampUTC($this->createdTime);
     }
 }
diff --git a/src/Forum/ForumTopicRedirects.php b/src/Forum/ForumTopicRedirects.php
index e0e8fa34..a28d8c14 100644
--- a/src/Forum/ForumTopicRedirects.php
+++ b/src/Forum/ForumTopicRedirects.php
@@ -65,7 +65,7 @@ class ForumTopicRedirects {
 
     public function hasTopicRedirect(ForumTopicInfo|string $topicInfo): bool {
         if($topicInfo instanceof ForumTopicInfo)
-            $topicInfo = $topicInfo->getId();
+            $topicInfo = $topicInfo->id;
 
         $stmt = $this->cache->get('SELECT COUNT(*) FROM msz_forum_topics_redirects WHERE topic_id = ?');
         $stmt->addParameter(1, $topicInfo);
@@ -80,7 +80,7 @@ class ForumTopicRedirects {
 
     public function getTopicRedirect(ForumTopicInfo|string $topicInfo): ForumTopicRedirectInfo {
         if($topicInfo instanceof ForumTopicInfo)
-            $topicInfo = $topicInfo->getId();
+            $topicInfo = $topicInfo->id;
 
         $stmt = $this->cache->get('SELECT topic_id, user_id, topic_redir_url, UNIX_TIMESTAMP(topic_redir_created) FROM msz_forum_topics_redirects WHERE topic_id = ?');
         $stmt->addParameter(1, $topicInfo);
@@ -99,7 +99,7 @@ class ForumTopicRedirects {
         string $linkTarget
     ): ForumTopicRedirectInfo {
         if($topicInfo instanceof ForumTopicInfo)
-            $topicInfo = $topicInfo->getId();
+            $topicInfo = $topicInfo->id;
         if($userInfo instanceof UserInfo)
             $userInfo = $userInfo->getId();
 
@@ -114,9 +114,9 @@ class ForumTopicRedirects {
 
     public function deleteTopicRedirect(ForumTopicRedirectInfo|ForumTopicInfo|string $topicInfo): void {
         if($topicInfo instanceof ForumTopicRedirectInfo)
-            $topicInfo = $topicInfo->getTopicId();
+            $topicInfo = $topicInfo->topicId;
         elseif($topicInfo instanceof ForumTopicInfo)
-            $topicInfo = $topicInfo->getId();
+            $topicInfo = $topicInfo->id;
 
         $stmt = $this->cache->get('DELETE FROM msz_forum_topics_redirects WHERE topic_id = ?');
         $stmt->addParameter(1, $topicInfo);
diff --git a/src/Forum/ForumTopics.php b/src/Forum/ForumTopics.php
index bbf869ce..2c4dd946 100644
--- a/src/Forum/ForumTopics.php
+++ b/src/Forum/ForumTopics.php
@@ -24,7 +24,7 @@ class ForumTopics {
         ?bool $deleted = null
     ): int {
         if($categoryInfo instanceof ForumCategoryInfo)
-            $categoryInfo = $categoryInfo->getId();
+            $categoryInfo = $categoryInfo->id;
         if($userInfo instanceof UserInfo)
             $userInfo = $userInfo->getId();
 
@@ -70,7 +70,7 @@ class ForumTopics {
         if($hasCategoryInfo) {
             if(is_array($categoryInfo)) {
                 foreach($categoryInfo as $categoryInfoEntry)
-                    $stmt->addParameter(++$args, $categoryInfoEntry instanceof ForumCategoryInfo ? $categoryInfoEntry->getId() : (string)$categoryInfoEntry);
+                    $stmt->addParameter(++$args, $categoryInfoEntry instanceof ForumCategoryInfo ? $categoryInfoEntry->id : (string)$categoryInfoEntry);
             } else
                 $stmt->addParameter(++$args, $categoryInfo);
         }
@@ -118,7 +118,7 @@ class ForumTopics {
         }
 
         if($categoryInfo instanceof ForumCategoryInfo)
-            $categoryInfo = $categoryInfo->getId();
+            $categoryInfo = $categoryInfo->id;
         if($userInfo instanceof UserInfo)
             $userInfo = $userInfo->getId();
 
@@ -176,7 +176,7 @@ class ForumTopics {
         if($hasCategoryInfo) {
             if(is_array($categoryInfo)) {
                 foreach($categoryInfo as $categoryInfoEntry)
-                    $stmt->addParameter(++$args, $categoryInfoEntry instanceof ForumCategoryInfo ? $categoryInfoEntry->getId() : (string)$categoryInfoEntry);
+                    $stmt->addParameter(++$args, $categoryInfoEntry instanceof ForumCategoryInfo ? $categoryInfoEntry->id : (string)$categoryInfoEntry);
             } else
                 $stmt->addParameter(++$args, $categoryInfo);
         }
@@ -216,7 +216,7 @@ class ForumTopics {
         if($hasPostInfo) {
             if($postInfo instanceof ForumPostInfo) {
                 $query .= ' WHERE topic_id = ?';
-                $value = $postInfo->getTopicId();
+                $value = $postInfo->topicId;
             } else {
                 $query .= ' WHERE topic_id = (SELECT topic_id FROM msz_forum_posts WHERE post_id = ?)';
                 $value = $postInfo;
@@ -246,7 +246,7 @@ class ForumTopics {
             $type = ForumTopicInfo::TYPE_ALIASES[$type];
         }
         if($categoryInfo instanceof ForumCategoryInfo)
-            $categoryInfo = $categoryInfo->getId();
+            $categoryInfo = $categoryInfo->id;
         if($userInfo instanceof UserInfo)
             $userInfo = $userInfo->getId();
 
@@ -266,7 +266,7 @@ class ForumTopics {
         string|int|null $type = null
     ): void {
         if($topicInfo instanceof ForumTopicInfo)
-            $topicInfo = $topicInfo->getId();
+            $topicInfo = $topicInfo->id;
 
         $fields = [];
         $values = [];
@@ -301,7 +301,7 @@ class ForumTopics {
 
     public function incrementTopicViews(ForumTopicInfo|string $topicInfo): void {
         if($topicInfo instanceof ForumTopicInfo)
-            $topicInfo = $topicInfo->getId();
+            $topicInfo = $topicInfo->id;
 
         $stmt = $this->cache->get('UPDATE msz_forum_topics SET topic_count_views = topic_count_views + 1 WHERE topic_id = ?');
         $stmt->addParameter(1, $topicInfo);
@@ -310,7 +310,7 @@ class ForumTopics {
 
     public function bumpTopic(ForumTopicInfo|string $topicInfo): void {
         if($topicInfo instanceof ForumTopicInfo)
-            $topicInfo = $topicInfo->getId();
+            $topicInfo = $topicInfo->id;
 
         $stmt = $this->cache->get('UPDATE msz_forum_topics SET topic_bumped = NOW() WHERE topic_id = ?');
         $stmt->addParameter(1, $topicInfo);
@@ -319,7 +319,7 @@ class ForumTopics {
 
     public function lockTopic(ForumTopicInfo|string $topicInfo): void {
         if($topicInfo instanceof ForumTopicInfo)
-            $topicInfo = $topicInfo->getId();
+            $topicInfo = $topicInfo->id;
 
         $stmt = $this->cache->get('UPDATE msz_forum_topics SET topic_locked = NOW() WHERE topic_id = ?');
         $stmt->addParameter(1, $topicInfo);
@@ -328,7 +328,7 @@ class ForumTopics {
 
     public function unlockTopic(ForumTopicInfo|string $topicInfo): void {
         if($topicInfo instanceof ForumTopicInfo)
-            $topicInfo = $topicInfo->getId();
+            $topicInfo = $topicInfo->id;
 
         $stmt = $this->cache->get('UPDATE msz_forum_topics SET topic_locked = NULL WHERE topic_id = ?');
         $stmt->addParameter(1, $topicInfo);
@@ -337,7 +337,7 @@ class ForumTopics {
 
     public function deleteTopic(ForumTopicInfo|string $topicInfo): void {
         if($topicInfo instanceof ForumTopicInfo)
-            $topicInfo = $topicInfo->getId();
+            $topicInfo = $topicInfo->id;
 
         $stmt = $this->cache->get('UPDATE msz_forum_topics SET topic_deleted = COALESCE(topic_deleted, NOW()) WHERE topic_id = ? AND topic_deleted IS NULL');
         $stmt->addParameter(1, $topicInfo);
@@ -350,7 +350,7 @@ class ForumTopics {
 
     public function restoreTopic(ForumTopicInfo|string $topicInfo): void {
         if($topicInfo instanceof ForumTopicInfo)
-            $topicInfo = $topicInfo->getId();
+            $topicInfo = $topicInfo->id;
 
         $stmt = $this->cache->get('UPDATE msz_forum_posts AS fp SET post_deleted = NULL WHERE topic_id = ? AND post_deleted = (SELECT topic_deleted FROM msz_forum_topics WHERE topic_id = fp.topic_id)');
         $stmt->addParameter(1, $topicInfo);
@@ -363,7 +363,7 @@ class ForumTopics {
 
     public function nukeTopic(ForumTopicInfo|string $topicInfo): void {
         if($topicInfo instanceof ForumTopicInfo)
-            $topicInfo = $topicInfo->getId();
+            $topicInfo = $topicInfo->id;
 
         $stmt = $this->cache->get('DELETE FROM msz_forum_topics WHERE topic_id = ?');
         $stmt->addParameter(1, $topicInfo);
@@ -377,7 +377,7 @@ class ForumTopics {
         if($userInfo === null)
             return false;
         if($topicInfo instanceof ForumTopicInfo)
-            $topicInfo = $topicInfo->getId();
+            $topicInfo = $topicInfo->id;
         if($userInfo instanceof UserInfo)
             $userInfo = $userInfo->getId();
 
@@ -398,7 +398,7 @@ class ForumTopics {
             return false;
 
         $topicInfoIsInstance = $topicInfo instanceof ForumTopicInfo;
-        if($topicInfoIsInstance && !$topicInfo->isActive())
+        if($topicInfoIsInstance && !$topicInfo->active)
             return false;
 
         $query = 'SELECT UNIX_TIMESTAMP(track_last_read) FROM msz_forum_topics_track AS ftt WHERE user_id = ? AND topic_id = ?';
@@ -407,7 +407,7 @@ class ForumTopics {
 
         $stmt = $this->cache->get($query);
         $stmt->addParameter(1, $userInfo instanceof UserInfo ? $userInfo->getId() : $userInfo);
-        $stmt->addParameter(2, $topicInfoIsInstance ? $topicInfo->getId() : $topicInfo);
+        $stmt->addParameter(2, $topicInfoIsInstance ? $topicInfo->id : $topicInfo);
         $stmt->execute();
         $result = $stmt->getResult();
 
@@ -415,7 +415,7 @@ class ForumTopics {
         if(!$result->next())
             return true;
 
-        return $result->getInteger(0) < $topicInfo->getBumpedTime();
+        return $result->getInteger(0) < $topicInfo->bumpedTime;
     }
 
     public function getMostActiveTopicInfo(
@@ -445,7 +445,7 @@ class ForumTopics {
         $stmt->addParameter(++$args, $userInfo);
         foreach($exceptCategoryInfos as $categoryInfo) {
             if($categoryInfo instanceof ForumCategoryInfo)
-                $stmt->addParameter(++$args, $categoryInfo->getId());
+                $stmt->addParameter(++$args, $categoryInfo->id);
             elseif(is_string($categoryInfo) || is_int($categoryInfo))
                 $stmt->addParameter(++$args, (string)$categoryInfo);
             else
@@ -453,7 +453,7 @@ class ForumTopics {
         }
         foreach($exceptTopicInfos as $topicInfo) {
             if($topicInfo instanceof ForumTopicInfo)
-                $stmt->addParameter(++$args, $topicInfo->getId());
+                $stmt->addParameter(++$args, $topicInfo->id);
             elseif(is_string($topicInfo) || is_int($topicInfo))
                 $stmt->addParameter(++$args, (string)$topicInfo);
             else
@@ -483,7 +483,7 @@ class ForumTopics {
             return true;
 
         $stmt = $this->cache->get('SELECT COUNT(*) FROM msz_forum_topics_track WHERE topic_id = ? AND user_id = ?');
-        $stmt->addParameter(1, $topicInfo instanceof ForumTopicInfo ? $topicInfo->getId() : $topicInfo);
+        $stmt->addParameter(1, $topicInfo instanceof ForumTopicInfo ? $topicInfo->id : $topicInfo);
         $stmt->addParameter(2, $userInfo instanceof UserInfo ? $userInfo->getId() : $userInfo);
         $stmt->execute();
         $result = $stmt->getResult();
@@ -503,13 +503,13 @@ class ForumTopics {
             $userInfo = $userInfo->getId();
 
         if($topicInfo instanceof ForumTopicInfo) {
-            $categoryInfo = $topicInfo->getCategoryId();
-            $topicInfo = $topicInfo->getId();
+            $categoryInfo = $topicInfo->categoryId;
+            $topicInfo = $topicInfo->id;
         } else {
             if($categoryInfo === null)
                 throw new InvalidArgumentException('$categoryInfo must be specified if $topicInfo is not an instance of ForumTopicInfo.');
             if($categoryInfo instanceof ForumCategoryInfo)
-                $categoryInfo = $categoryInfo->getId();
+                $categoryInfo = $categoryInfo->id;
         }
 
         $stmt = $this->cache->get('REPLACE INTO msz_forum_topics_track (user_id, topic_id, forum_id, track_last_read) VALUES (?, ?, ?, NOW())');
diff --git a/src/Hanyuu/HanyuuRpcHandler.php b/src/Hanyuu/HanyuuRpcHandler.php
index 201b4ba5..f36989d4 100644
--- a/src/Hanyuu/HanyuuRpcHandler.php
+++ b/src/Hanyuu/HanyuuRpcHandler.php
@@ -41,7 +41,7 @@ final class HanyuuRpcHandler implements RpcHandler {
     }
 
     private function canImpersonateUserId(UserInfo $impersonator, string $targetId): bool {
-        if($impersonator->isSuperUser())
+        if($impersonator->super)
             return true;
 
         return in_array(
@@ -78,28 +78,26 @@ final class HanyuuRpcHandler implements RpcHandler {
 
         $tokenPacker = $this->authCtx->createAuthTokenPacker();
         $tokenInfo = $tokenPacker->unpack(trim($token));
-        if($tokenInfo->isEmpty())
+        if($tokenInfo->isEmpty)
             return self::createPayload('auth:check:fail', ['reason' => 'empty', 'login_url' => $loginUrl, 'register_url' => $registerUrl]);
 
-        $sessions = $this->authCtx->getSessions();
         try {
-            $sessionInfo = $sessions->getSession(sessionToken: $tokenInfo->getSessionToken());
+            $sessionInfo = $this->authCtx->sessions->getSession(sessionToken: $tokenInfo->sessionToken);
         } catch(RuntimeException $ex) {
             return self::createPayload('auth:check:fail', ['reason' => 'session', 'login_url' => $loginUrl, 'register_url' => $registerUrl]);
         }
 
-        if($sessionInfo->hasExpired()) {
-            $sessions->deleteSessions(sessionInfos: $sessionInfo);
+        if($sessionInfo->expired) {
+            $this->authCtx->sessions->deleteSessions(sessionInfos: $sessionInfo);
             return self::createPayload('auth:check:fail', ['reason' => 'expired', 'login_url' => $loginUrl, 'register_url' => $registerUrl]);
         }
 
-        $sessions->recordSessionActivity(sessionInfo: $sessionInfo, remoteAddr: $remoteAddr);
+        $this->authCtx->sessions->recordSessionActivity(sessionInfo: $sessionInfo, remoteAddr: $remoteAddr);
 
-        $users = $this->usersCtx->getUsers();
-        $userInfo = $userInfoReal = $users->getUser($sessionInfo->getUserId(), 'id');
-        if($tokenInfo->hasImpersonatedUserId() && $this->canImpersonateUserId($userInfo, $tokenInfo->getImpersonatedUserId())) {
+        $userInfo = $userInfoReal = $this->usersCtx->users->getUser($sessionInfo->userId, 'id');
+        if($tokenInfo->hasImpersonatedUserId && $this->canImpersonateUserId($userInfo, $tokenInfo->impersonatedUserId)) {
             try {
-                $userInfo = $users->getUser($tokenInfo->getImpersonatedUserId(), 'id');
+                $userInfo = $this->usersCtx->users->getUser($tokenInfo->impersonatedUserId, 'id');
             } catch(RuntimeException $ex) {
                 $userInfo = $userInfoReal;
             }
@@ -107,22 +105,22 @@ final class HanyuuRpcHandler implements RpcHandler {
 
         $response = [];
         $response['session'] = [
-            'token' => $sessionInfo->getToken(),
-            'created_at' => $sessionInfo->getCreatedTime(),
-            'expires_at' => $sessionInfo->getExpiresTime(),
-            'lifetime_extends' => $sessionInfo->shouldBumpExpires(),
+            'token' => $sessionInfo->token,
+            'created_at' => $sessionInfo->createdTime,
+            'expires_at' => $sessionInfo->expiresTime,
+            'lifetime_extends' => $sessionInfo->shouldBumpExpires,
         ];
 
         $banInfo = $this->usersCtx->tryGetActiveBan($userInfo);
         if($banInfo !== null)
             $response['ban'] = [
-                'severity' => $banInfo->getSeverity(),
-                'reason' => $banInfo->getPublicReason(),
-                'created_at' => $banInfo->getCreatedTime(),
-                'is_permanent' => $banInfo->isPermanent(),
-                'expires_at' => $banInfo->getExpiresTime(),
-                'duration_str' => $banInfo->getDurationString(),
-                'remaining_str' => $banInfo->getRemainingString(),
+                'severity' => $banInfo->severity,
+                'reason' => $banInfo->publicReason,
+                'created_at' => $banInfo->createdTime,
+                'is_permanent' => $banInfo->permanent,
+                'expires_at' => $banInfo->expiresTime,
+                'duration_str' => $banInfo->durationString,
+                'remaining_str' => $banInfo->remainingString,
             ];
 
         $gatherRequestedAvatars = function($userInfo) use ($avatarResolutions, $baseUrl) {
@@ -140,13 +138,13 @@ final class HanyuuRpcHandler implements RpcHandler {
 
         $extractUserInfo = fn($userInfo) => [
             'id' => $userInfo->getId(),
-            'name' => $userInfo->getName(),
+            'name' => $userInfo->name,
             'colour' => (string)$users->getUserColour($userInfo),
             'rank' => $users->getUserRank($userInfo),
-            'is_super' => $userInfo->isSuperUser(),
-            'country_code' => $userInfo->getCountryCode(),
-            'is_deleted' => $userInfo->isDeleted(),
-            'has_totp' => $userInfo->hasTOTPKey(),
+            'is_super' => $userInfo->super,
+            'country_code' => $userInfo->countryCode,
+            'is_deleted' => $userInfo->deleted,
+            'has_totp' => $userInfo->hasTOTP,
             'profile_url' => $baseUrl . $this->urls->format('user-profile', ['user' => $userInfo->getId()]),
             'avatars' => $gatherRequestedAvatars($userInfo),
         ];
@@ -155,7 +153,7 @@ final class HanyuuRpcHandler implements RpcHandler {
         if($userInfo !== $userInfoReal) {
             $response['guise'] = $extractUserInfo($userInfoReal);
 
-            $csrfp = CSRF::create($sessionInfo->getToken());
+            $csrfp = CSRF::create($sessionInfo->token);
             $response['guise']['revert_url'] = $baseUrl . $this->urls->format('auth-revert', ['csrf' => $csrfp->createToken()]);
         }
 
diff --git a/src/Home/HomeRoutes.php b/src/Home/HomeRoutes.php
index 0e2c857c..44ac4600 100644
--- a/src/Home/HomeRoutes.php
+++ b/src/Home/HomeRoutes.php
@@ -42,7 +42,7 @@ class HomeRoutes implements RouteHandler, UrlSource {
     }
 
     private function getOnlineUsers(): iterable {
-        return $this->usersCtx->getUsers()->getUsers(
+        return $this->usersCtx->users->getUsers(
             lastActiveInMinutes: 5,
             deleted: false,
             orderBy: 'random',
@@ -63,16 +63,16 @@ class HomeRoutes implements RouteHandler, UrlSource {
         $posts = [];
 
         foreach($postInfos as $postInfo) {
-            $categoryId = $postInfo->getCategoryId();
-            $userInfo = $postInfo->hasUserId() ? $this->usersCtx->getUserInfo($postInfo->getUserId()) : null;
+            $categoryId = $postInfo->categoryId;
+            $userInfo = $postInfo->userId !== null ? $this->usersCtx->getUserInfo($postInfo->userId) : null;
 
             if(array_key_exists($categoryId, $this->newsCategoryInfos))
                 $categoryInfo = $this->newsCategoryInfos[$categoryId];
             else
                 $this->newsCategoryInfos[$categoryId] = $categoryInfo = $this->news->getCategory(postInfo: $postInfo);
 
-            $commentsCount = $postInfo->hasCommentsCategoryId()
-                ? $this->comments->countPosts(categoryInfo: $postInfo->getCommentsCategoryId(), deleted: false) : 0;
+            $commentsCount = $postInfo->commentsSectionId
+                ? $this->comments->countPosts(categoryInfo: $postInfo->commentsSectionId, deleted: false) : 0;
 
             $posts[] = [
                 'post' => $postInfo,
@@ -155,7 +155,7 @@ class HomeRoutes implements RouteHandler, UrlSource {
     #[HttpGet('/')]
     #[UrlFormat('index', '/')]
     public function getIndex(...$args) {
-        return $this->authInfo->isLoggedIn()
+        return $this->authInfo->isLoggedIn
             ? $this->getHome(...$args)
             : $this->getLanding(...$args);
     }
@@ -169,7 +169,7 @@ class HomeRoutes implements RouteHandler, UrlSource {
         $stats['users:online:recent'] = count($onlineUserInfos);
 
         $birthdays = [];
-        $birthdayInfos = $this->usersCtx->getUsers()->getUsers(deleted: false, birthdate: XDateTime::now(), orderBy: 'random');
+        $birthdayInfos = $this->usersCtx->users->getUsers(deleted: false, birthdate: XDateTime::now(), orderBy: 'random');
         foreach($birthdayInfos as $birthdayInfo)
             $birthdays[] = [
                 'info' => $birthdayInfo,
@@ -211,9 +211,9 @@ class HomeRoutes implements RouteHandler, UrlSource {
             $linkedData = [
                 '@context' => 'http://schema.org',
                 '@type' => 'Organization',
-                'name' => $this->siteInfo->getName(),
-                'url' => $this->siteInfo->getURL(),
-                'logo' => $this->siteInfo->getExternalLogo(),
+                'name' => $this->siteInfo->name,
+                'url' => $this->siteInfo->url,
+                'logo' => $this->siteInfo->externalLogo,
                 'same_as' => $config['social.linked'],
             ];
         } else $linkedData = null;
diff --git a/src/Imaging/GdImage.php b/src/Imaging/GdImage.php
index 255d4992..90b73147 100644
--- a/src/Imaging/GdImage.php
+++ b/src/Imaging/GdImage.php
@@ -42,9 +42,8 @@ final class GdImage extends Image {
             }
         }
 
-        if(!isset($this->gd)) {
+        if(!isset($this->gd))
             throw new InvalidArgumentException('Unsupported image format.');
-        }
     }
 
     public function __destruct() {
diff --git a/src/Messages/MessageInfo.php b/src/Messages/MessageInfo.php
index caa804ee..f5f1d520 100644
--- a/src/Messages/MessageInfo.php
+++ b/src/Messages/MessageInfo.php
@@ -7,146 +7,82 @@ use Index\Db\DbResult;
 
 class MessageInfo {
     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 private(set) string $id,
+        public private(set) string $ownerId,
+        public private(set) ?string $authorId,
+        public private(set) ?string $recipientId,
+        public private(set) ?string $replyToId,
+        public private(set) string $title,
+        public private(set) string $body,
+        public private(set) int $parser,
+        public private(set) int $createdTime,
+        public private(set) ?int $sentTime,
+        public private(set) ?int $readTime,
+        public private(set) ?int $deletedTime,
     ) {}
 
     public static function fromResult(DbResult $result): MessageInfo {
         return new MessageInfo(
-            messageId: $result->getString(0),
+            id: $result->getString(0),
             ownerId: $result->getString(1),
             authorId: $result->getStringOrNull(2),
             recipientId: $result->getStringOrNull(3),
-            replyTo: $result->getStringOrNull(4),
+            replyToId: $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),
+            createdTime: $result->getInteger(8),
+            sentTime: $result->getIntegerOrNull(9),
+            readTime: $result->getIntegerOrNull(10),
+            deletedTime: $result->getIntegerOrNull(11),
         );
     }
 
-    public function getId(): string {
-        return $this->messageId;
+    public bool $isBodyPlain {
+        get => $this->parser === Parser::PLAIN;
     }
 
-    public function getOwnerId(): string {
-        return $this->ownerId;
+    public bool $isBodyBBCode {
+        get => $this->parser === Parser::BBCODE;
     }
 
-    public function hasAuthorId(): bool {
-        return $this->authorId !== null;
+    public bool $isBodyMarkdown {
+        get => $this->parser === Parser::MARKDOWN;
     }
 
-    public function getAuthorId(): ?string {
-        return $this->authorId;
+    public CarbonImmutable $createdAt {
+        get => CarbonImmutable::createFromTimestampUTC($this->createdTime);
     }
 
-    public function hasRecipientId(): bool {
-        return $this->recipientId !== null;
+    public bool $sent {
+        get => $this->sentTime !== null;
     }
 
-    public function getRecipientId(): ?string {
-        return $this->recipientId;
+    public ?CarbonImmutable $sentAt {
+        get => $this->sentTime === null ? null : CarbonImmutable::createFromTimestampUTC($this->sentTime);
     }
 
-    public function hasReplyToId(): bool {
-        return $this->replyTo !== null;
+    public bool $read {
+        get => $this->readTime !== null;
     }
 
-    public function getReplyToId(): ?string {
-        return $this->replyTo;
+    public ?CarbonImmutable $readAt {
+        get => $this->readTime === null ? null : CarbonImmutable::createFromTimestampUTC($this->readTime);
     }
 
-    public function getTitle(): string {
-        return $this->title;
+    public bool $deleted {
+        get => $this->deletedTime !== null;
     }
 
-    public function getBody(): string {
-        return $this->body;
+    public ?CarbonImmutable $deletedAt {
+        get => $this->deletedTime === null ? null : CarbonImmutable::createFromTimestampUTC($this->deletedTime);
     }
 
-    public function getParser(): int {
-        return $this->parser;
+    public int $displayTime {
+        get => $this->sent ? $this->sentTime : $this->createdTime;
     }
 
-    public function isBodyPlain(): bool {
-        return $this->parser === Parser::PLAIN;
-    }
-
-    public function isBodyBBCode(): bool {
-        return $this->parser === Parser::BBCODE;
-    }
-
-    public function isBodyMarkdown(): bool {
-        return $this->parser === Parser::MARKDOWN;
-    }
-
-    public function getCreatedTime(): int {
-        return $this->created;
-    }
-
-    public function getCreatedAt(): CarbonImmutable {
-        return CarbonImmutable::createFromTimestampUTC($this->created);
-    }
-
-    public function isSent(): bool {
-        return $this->sent !== null;
-    }
-
-    public function getSentTime(): ?int {
-        return $this->sent;
-    }
-
-    public function getSentAt(): ?CarbonImmutable {
-        return $this->sent === null ? null : CarbonImmutable::createFromTimestampUTC($this->sent);
-    }
-
-    public function isRead(): bool {
-        return $this->read !== null;
-    }
-
-    public function getReadTime(): ?int {
-        return $this->read;
-    }
-
-    public function getReadAt(): ?CarbonImmutable {
-        return $this->read === null ? null : CarbonImmutable::createFromTimestampUTC($this->read);
-    }
-
-    public function isDeleted(): bool {
-        return $this->deleted !== null;
-    }
-
-    public function getDeletedTime(): ?int {
-        return $this->deleted;
-    }
-
-    public function getDeletedAt(): ?CarbonImmutable {
-        return $this->deleted === null ? null : CarbonImmutable::createFromTimestampUTC($this->deleted);
-    }
-
-    public function getDisplayTime(): int {
-        if($this->isSent())
-            return $this->getSentTime();
-        return $this->getCreatedTime();
-    }
-
-    public function getDisplayAt(): CarbonImmutable {
-        if($this->isSent())
-            return $this->getSentAt();
-        return $this->getCreatedAt();
+    public CarbonImmutable $displayAt {
+        get => $this->sent ? $this->sentAt : $this->createdAt;
     }
 }
diff --git a/src/Messages/MessagesContext.php b/src/Messages/MessagesContext.php
index f1144cb5..f57a7ba4 100644
--- a/src/Messages/MessagesContext.php
+++ b/src/Messages/MessagesContext.php
@@ -4,13 +4,9 @@ namespace Misuzu\Messages;
 use Index\Db\DbConnection;
 
 class MessagesContext {
-    private MessagesDatabase $database;
+    public private(set) MessagesDatabase $database;
 
     public function __construct(DbConnection $dbConn) {
         $this->database = new MessagesDatabase($dbConn);
     }
-
-    public function getDatabase(): MessagesDatabase {
-        return $this->database;
-    }
 }
diff --git a/src/Messages/MessagesDatabase.php b/src/Messages/MessagesDatabase.php
index c634fb74..9ee7f3a3 100644
--- a/src/Messages/MessagesDatabase.php
+++ b/src/Messages/MessagesDatabase.php
@@ -72,9 +72,9 @@ class MessagesDatabase {
         if($hasRecipientInfo)
             $stmt->addParameter(++$args, $recipientInfo instanceof UserInfo ? $recipientInfo->getId() : $recipientInfo);
         if($hasRepliesFor)
-            $stmt->addParameter(++$args, $repliesFor instanceof MessageInfo ? $repliesFor->getId() : $repliesFor);
+            $stmt->addParameter(++$args, $repliesFor instanceof MessageInfo ? $repliesFor->id : $repliesFor);
         if($hasReplyTo)
-            $stmt->addParameter(++$args, $replyTo instanceof MessageInfo ? $replyTo->getReplyToId() : $replyTo);
+            $stmt->addParameter(++$args, $replyTo instanceof MessageInfo ? $replyTo->replyToId : $replyTo);
 
         $stmt->execute();
         $result = $stmt->getResult();
@@ -143,9 +143,9 @@ class MessagesDatabase {
         if($hasRecipientInfo)
             $stmt->addParameter(++$args, $recipientInfo instanceof UserInfo ? $recipientInfo->getId() : $recipientInfo);
         if($hasRepliesFor)
-            $stmt->addParameter(++$args, $repliesFor instanceof MessageInfo ? $repliesFor->getId() : $repliesFor);
+            $stmt->addParameter(++$args, $repliesFor instanceof MessageInfo ? $repliesFor->id : $repliesFor);
         if($hasReplyTo)
-            $stmt->addParameter(++$args, $replyTo instanceof MessageInfo ? $replyTo->getReplyToId() : $replyTo);
+            $stmt->addParameter(++$args, $replyTo instanceof MessageInfo ? $replyTo->replyToId : $replyTo);
         if($hasPagination) {
             $stmt->addParameter(++$args, $pagination->getRange());
             $stmt->addParameter(++$args, $pagination->getOffset());
@@ -167,7 +167,7 @@ class MessagesDatabase {
         ));
 
         if($messageInfoOrId instanceof MessageInfo)
-            $stmt->addParameter(1, $useReplyTo ? $messageInfoOrId->getReplyToId() : $messageInfoOrId->getId());
+            $stmt->addParameter(1, $useReplyTo ? $messageInfoOrId->replyToId : $messageInfoOrId->id);
         else
             $stmt->addParameter(1, $messageInfoOrId);
         $stmt->addParameter(2, $ownerInfo instanceof UserInfo ? $ownerInfo->getId() : $ownerInfo);
@@ -197,7 +197,7 @@ class MessagesDatabase {
         $stmt->addParameter(2, $ownerInfo instanceof UserInfo ? $ownerInfo->getId() : $ownerInfo);
         $stmt->addParameter(3, $authorInfo instanceof UserInfo ? $authorInfo->getId() : $authorInfo);
         $stmt->addParameter(4, $recipientInfo instanceof UserInfo ? $recipientInfo->getId() : $recipientInfo);
-        $stmt->addParameter(5, $replyTo instanceof MessageInfo ? $replyTo->getId() : $replyTo);
+        $stmt->addParameter(5, $replyTo instanceof MessageInfo ? $replyTo->id : $replyTo);
         $stmt->addParameter(6, $title);
         $stmt->addParameter(7, $body);
         $stmt->addParameter(8, $parser);
@@ -229,7 +229,7 @@ class MessagesDatabase {
 
         if($messageInfo !== null) {
             $whereQuery[] = 'msg_id = ?';
-            $whereValues[] = $messageInfo instanceof MessageInfo ? $messageInfo->getId() : $messageInfo;
+            $whereValues[] = $messageInfo instanceof MessageInfo ? $messageInfo->id : $messageInfo;
         }
 
         if($title !== null) {
@@ -307,7 +307,7 @@ class MessagesDatabase {
                 if(is_string($messageInfo))
                     $stmt->addParameter(++$args, $messageInfo);
                 elseif($messageInfo instanceof MessageInfo)
-                    $stmt->addParameter(++$args, $messageInfo->getId());
+                    $stmt->addParameter(++$args, $messageInfo->id);
                 else
                     throw new InvalidArgumentException('$messageInfos must be an array of strings or MessageInfo instances.');
             }
@@ -345,7 +345,7 @@ class MessagesDatabase {
                 if(is_string($messageInfo))
                     $stmt->addParameter(++$args, $messageInfo);
                 elseif($messageInfo instanceof MessageInfo)
-                    $stmt->addParameter(++$args, $messageInfo->getId());
+                    $stmt->addParameter(++$args, $messageInfo->id);
                 else
                     throw new InvalidArgumentException('$messageInfos must be an array of strings or MessageInfo instances.');
             }
@@ -383,7 +383,7 @@ class MessagesDatabase {
                 if(is_string($messageInfo))
                     $stmt->addParameter(++$args, $messageInfo);
                 elseif($messageInfo instanceof MessageInfo)
-                    $stmt->addParameter(++$args, $messageInfo->getId());
+                    $stmt->addParameter(++$args, $messageInfo->id);
                 else
                     throw new InvalidArgumentException('$messageInfos must be an array of strings or MessageInfo instances.');
             }
diff --git a/src/Messages/MessagesRoutes.php b/src/Messages/MessagesRoutes.php
index 0a984647..1aa5a734 100644
--- a/src/Messages/MessagesRoutes.php
+++ b/src/Messages/MessagesRoutes.php
@@ -38,11 +38,11 @@ class MessagesRoutes implements RouteHandler, UrlSource {
     #[HttpMiddleware('/messages')]
     public function checkAccess($response, $request) {
         // should probably be a permission or something too
-        if(!$this->authInfo->isLoggedIn())
+        if(!$this->authInfo->isLoggedIn)
             return 401;
 
         // do not allow access to PMs when impersonating in production mode
-        if(!MSZ_DEBUG && $this->authInfo->isImpersonating())
+        if(!MSZ_DEBUG && $this->authInfo->isImpersonating)
             return 403;
 
         $globalPerms = $this->authInfo->getPerms('global');
@@ -50,7 +50,7 @@ class MessagesRoutes implements RouteHandler, UrlSource {
             return 403;
 
         $this->canSendMessages = $globalPerms->check(Perm::G_MESSAGES_SEND)
-            && !$this->usersCtx->hasActiveBan($this->authInfo->getUserInfo());
+            && !$this->usersCtx->hasActiveBan($this->authInfo->userInfo);
 
         if($request->getMethod() === 'POST' && $request->isFormContent()) {
             $content = $request->getContent();
@@ -70,9 +70,9 @@ class MessagesRoutes implements RouteHandler, UrlSource {
         $message = new stdClass;
 
         $message->info = $messageInfo;
-        $message->author_info = $messageInfo->hasAuthorId() ? $this->usersCtx->getUserInfo($messageInfo->getAuthorId(), 'id') : null;
+        $message->author_info = $messageInfo->authorId !== null ? $this->usersCtx->getUserInfo($messageInfo->authorId, 'id') : null;
         $message->author_colour = $this->usersCtx->getUserColour($message->author_info);
-        $message->recipient_info = $messageInfo->hasRecipientId() ? $this->usersCtx->getUserInfo($messageInfo->getRecipientId(), 'id') : null;
+        $message->recipient_info = $messageInfo->recipientId !== null ? $this->usersCtx->getUserInfo($messageInfo->recipientId, 'id') : null;
         $message->recipient_colour = $this->usersCtx->getUserColour($message->recipient_info);
 
         return $message;
@@ -93,8 +93,8 @@ class MessagesRoutes implements RouteHandler, UrlSource {
         $folderSent = $folderName === 'sent';
         $folderTrash = $folderName === 'trash';
 
-        $selfInfo = $this->authInfo->getUserInfo();
-        $msgsDb = $this->msgsCtx->getDatabase();
+        $selfInfo = $this->authInfo->userInfo;
+        $msgsDb = $this->msgsCtx->database;
 
         $authorInfo = !$folderTrash && $folderSent ? $selfInfo : null;
         $recipientInfo = !$folderTrash && $folderInbox ? $selfInfo : null;
@@ -134,8 +134,8 @@ class MessagesRoutes implements RouteHandler, UrlSource {
     #[HttpGet('/messages/stats')]
     #[UrlFormat('messages-stats', '/messages/stats')]
     public function getStats() {
-        $selfInfo = $this->authInfo->getUserInfo();
-        $msgsDb = $this->msgsCtx->getDatabase();
+        $selfInfo = $this->authInfo->userInfo;
+        $msgsDb = $this->msgsCtx->database;
 
         return [
             'unread' => $msgsDb->countMessages(
@@ -179,7 +179,7 @@ class MessagesRoutes implements RouteHandler, UrlSource {
 
         return [
             'id' => $userInfo->getId(),
-            'name' => $userInfo->getName(),
+            'name' => $userInfo->name,
             'ban' => $this->usersCtx->hasActiveBan($userInfo),
             'avatar' => $this->urls->format('user-avatar', [
                 'user' => $userInfo->getId(),
@@ -205,8 +205,8 @@ class MessagesRoutes implements RouteHandler, UrlSource {
         if(strlen($messageId) !== 8)
             return 404;
 
-        $selfInfo = $this->authInfo->getUserInfo();
-        $msgsDb = $this->msgsCtx->getDatabase();
+        $selfInfo = $this->authInfo->userInfo;
+        $msgsDb = $this->msgsCtx->database;
 
         try {
             $messageInfo = $msgsDb->getMessageInfo($selfInfo, $messageId);
@@ -214,7 +214,7 @@ class MessagesRoutes implements RouteHandler, UrlSource {
             return 404;
         }
 
-        if(!$messageInfo->isRead())
+        if(!$messageInfo->read)
             $msgsDb->updateMessage(
                 ownerInfo: $selfInfo,
                 messageInfo: $messageInfo,
@@ -224,7 +224,7 @@ class MessagesRoutes implements RouteHandler, UrlSource {
         $message = $this->populateMessage($messageInfo);
 
         $replyTo = null;
-        if($messageInfo->hasReplyToId()) {
+        if($messageInfo->replyToId !== null) {
             try {
                 $replyTo = $this->populateMessage(
                     $msgsDb->getMessageInfo($selfInfo, $messageInfo, true)
@@ -243,7 +243,7 @@ class MessagesRoutes implements RouteHandler, UrlSource {
         foreach($repliesForInfos as $repliesForInfo) {
             $repliesFor[] = $this->populateMessage($repliesForInfo);
 
-            if(!$repliesForInfo->isSent() && $draftInfo === null)
+            if(!$repliesForInfo->sent && $draftInfo === null)
                 $draftInfo = $repliesForInfo;
         }
 
@@ -349,8 +349,8 @@ class MessagesRoutes implements RouteHandler, UrlSource {
         if($error !== null)
             return $error;
 
-        $selfInfo = $this->authInfo->getUserInfo();
-        $msgsDb = $this->msgsCtx->getDatabase();
+        $selfInfo = $this->authInfo->userInfo;
+        $msgsDb = $this->msgsCtx->database;
 
         try {
             $recipientInfo = $this->usersCtx->getUserInfo($recipient, 'messaging');
@@ -387,7 +387,7 @@ class MessagesRoutes implements RouteHandler, UrlSource {
                 ];
             }
 
-            if(!$replyToInfo->isSent())
+            if(!$replyToInfo->sent)
                 return [
                     'error' => [
                         'name' => 'msgs:draft_reply',
@@ -450,8 +450,8 @@ class MessagesRoutes implements RouteHandler, UrlSource {
         if($error !== null)
             return $error;
 
-        $selfInfo = $this->authInfo->getUserInfo();
-        $msgsDb = $this->msgsCtx->getDatabase();
+        $selfInfo = $this->authInfo->userInfo;
+        $msgsDb = $this->msgsCtx->database;
 
         try {
             $messageInfo = $msgsDb->getMessageInfo($selfInfo, $messageId);
@@ -464,7 +464,7 @@ class MessagesRoutes implements RouteHandler, UrlSource {
             ];
         }
 
-        if(!$messageInfo->hasAuthorId() || $messageInfo->getAuthorId() !== $selfInfo->getId())
+        if($messageInfo->authorId === null || $messageInfo->authorId !== $selfInfo->getId())
             return [
                 'error' => [
                     'name' => 'msgs:not_author',
@@ -472,7 +472,7 @@ class MessagesRoutes implements RouteHandler, UrlSource {
                 ],
             ];
 
-        if(!$messageInfo->hasRecipientId())
+        if($messageInfo->recipientId === null)
             return [
                 'error' => [
                     'name' => 'msgs:recipient_gone',
@@ -480,7 +480,7 @@ class MessagesRoutes implements RouteHandler, UrlSource {
                 ],
             ];
 
-        if($messageInfo->isSent())
+        if($messageInfo->sent)
             return [
                 'error' => [
                     'name' => 'msgs:not_draft',
@@ -488,7 +488,7 @@ class MessagesRoutes implements RouteHandler, UrlSource {
                 ],
             ];
 
-        $error = $this->checkCanReceiveMessages($messageInfo->getRecipientId());
+        $error = $this->checkCanReceiveMessages($messageInfo->recipientId);
         if($error !== null)
             return $error;
 
@@ -504,16 +504,16 @@ class MessagesRoutes implements RouteHandler, UrlSource {
         );
 
         // recipient copy
-        if($sentAt !== null && $messageInfo->getRecipientId() !== $selfInfo->getId())
+        if($sentAt !== null && $messageInfo->recipientId !== $selfInfo->getId())
             $msgsDb->createMessage(
                 messageId: $messageId,
-                ownerInfo: $messageInfo->getRecipientId(),
+                ownerInfo: $messageInfo->recipientId,
                 authorInfo: $selfInfo,
-                recipientInfo: $messageInfo->getRecipientId(),
+                recipientInfo: $messageInfo->recipientId,
                 title: $title,
                 body: $body,
                 parser: $parser,
-                replyTo: $messageInfo->getReplyToId(),
+                replyTo: $messageInfo->replyToId,
                 sentAt: $sentAt
             );
 
@@ -541,8 +541,8 @@ class MessagesRoutes implements RouteHandler, UrlSource {
                 ],
             ];
 
-        $selfInfo = $this->authInfo->getUserInfo();
-        $msgsDb = $this->msgsCtx->getDatabase();
+        $selfInfo = $this->authInfo->userInfo;
+        $msgsDb = $this->msgsCtx->database;
 
         foreach($messages as $messageId)
             $msgsDb->updateMessage(
@@ -573,8 +573,8 @@ class MessagesRoutes implements RouteHandler, UrlSource {
 
         $messages = explode(',', $messages);
 
-        $this->msgsCtx->getDatabase()->deleteMessages(
-            $this->authInfo->getUserInfo(),
+        $this->msgsCtx->database->deleteMessages(
+            $this->authInfo->userInfo,
             $messages
         );
 
@@ -600,8 +600,8 @@ class MessagesRoutes implements RouteHandler, UrlSource {
 
         $messages = explode(',', $messages);
 
-        $this->msgsCtx->getDatabase()->restoreMessages(
-            $this->authInfo->getUserInfo(),
+        $this->msgsCtx->database->restoreMessages(
+            $this->authInfo->userInfo,
             $messages
         );
 
@@ -627,8 +627,8 @@ class MessagesRoutes implements RouteHandler, UrlSource {
 
         $messages = explode(',', $messages);
 
-        $this->msgsCtx->getDatabase()->nukeMessages(
-            $this->authInfo->getUserInfo(),
+        $this->msgsCtx->database->nukeMessages(
+            $this->authInfo->userInfo,
             $messages
         );
 
diff --git a/src/MisuzuContext.php b/src/MisuzuContext.php
index 0ee291f9..c91bab4b 100644
--- a/src/MisuzuContext.php
+++ b/src/MisuzuContext.php
@@ -29,36 +29,34 @@ use Index\Urls\UrlRegistry;
 //  dunno if i want null checks some maybe some kind of init func should be called first like is the case
 //  with the http shit
 class MisuzuContext {
-    private DbConnection $dbConn;
-    private Config $config;
     private TplEnvironment $templating;
 
-    private AuditLog $auditLog;
-    private Counters $counters;
+    public private(set) AuditLog $auditLog;
+    public private(set) Counters $counters;
 
-    private Emotes $emotes;
-    private Changelog $changelog;
-    private News $news;
-    private Comments $comments;
+    public private(set) Emotes $emotes;
+    public private(set) Changelog $changelog;
+    public private(set) News $news;
+    public private(set) Comments $comments;
 
-    private AuthContext $authCtx;
-    private ForumContext $forumCtx;
+    public private(set) AuthContext $authCtx;
+    public private(set) ForumContext $forumCtx;
     private MessagesContext $messagesCtx;
-    private UsersContext $usersCtx;
+    public private(set) UsersContext $usersCtx;
 
-    private ProfileFields $profileFields;
+    public private(set) ProfileFields $profileFields;
 
-    private Permissions $perms;
-    private AuthInfo $authInfo;
-    private SiteInfo $siteInfo;
+    public private(set) Permissions $perms;
+    public private(set) AuthInfo $authInfo;
+    public private(set) SiteInfo $siteInfo;
 
     // this probably shouldn't be available
-    private UrlRegistry $urls;
-
-    public function __construct(DbConnection $dbConn, Config $config) {
-        $this->dbConn = $dbConn;
-        $this->config = $config;
+    public private(set) UrlRegistry $urls;
 
+    public function __construct(
+        public private(set) DbConnection $dbConn,
+        public private(set) Config $config
+    ) {
         $this->perms = new Permissions($dbConn);
         $this->authInfo = new AuthInfo($this->perms);
         $this->siteInfo = new SiteInfo($config->scopeTo('site'));
@@ -77,10 +75,6 @@ class MisuzuContext {
         $this->profileFields = new ProfileFields($dbConn);
     }
 
-    public function getDbConn(): DbConnection {
-        return $this->dbConn;
-    }
-
     public function getDbQueryCount(): int {
         $result = $this->dbConn->query('SHOW SESSION STATUS LIKE "Questions"');
         return $result->next() ? $result->getInteger(1) : 0;
@@ -94,69 +88,9 @@ class MisuzuContext {
         return new FsDbMigrationRepo(MSZ_MIGRATIONS);
     }
 
-    public function getUrls(): UrlRegistry {
-        return $this->urls;
-    }
-
-    public function getConfig(): Config {
-        return $this->config;
-    }
-
-    public function getEmotes(): Emotes {
-        return $this->emotes;
-    }
-
-    public function getChangelog(): Changelog {
-        return $this->changelog;
-    }
-
-    public function getNews(): News {
-        return $this->news;
-    }
-
-    public function getComments(): Comments {
-        return $this->comments;
-    }
-
-    public function getAuditLog(): AuditLog {
-        return $this->auditLog;
-    }
-
-    public function getCounters(): Counters {
-        return $this->counters;
-    }
-
-    public function getProfileFields(): ProfileFields {
-        return $this->profileFields;
-    }
-
-    public function getPerms(): Permissions {
-        return $this->perms;
-    }
-
-    public function getAuthContext(): AuthContext {
-        return $this->authCtx;
-    }
-
-    public function getUsersContext(): UsersContext {
-        return $this->usersCtx;
-    }
-
-    public function getForumContext(): ForumContext {
-        return $this->forumCtx;
-    }
-
-    public function getAuthInfo(): AuthInfo {
-        return $this->authInfo;
-    }
-
-    public function getSiteInfo(): SiteInfo {
-        return $this->siteInfo;
-    }
-
     public function createAuditLog(string $action, array $params = [], UserInfo|string|null $userInfo = null): void {
-        if($userInfo === null && $this->authInfo->isLoggedIn())
-            $userInfo = $this->authInfo->getUserInfo();
+        if($userInfo === null && $this->authInfo->isLoggedIn)
+            $userInfo = $this->authInfo->userInfo;
 
         $this->auditLog->createLog(
             $userInfo,
@@ -169,8 +103,8 @@ class MisuzuContext {
 
     private ?bool $hasManageAccess = null;
     public function hasManageAccess(): bool {
-        $this->hasManageAccess ??= $this->authInfo->isLoggedIn()
-            && !$this->usersCtx->hasActiveBan($this->authInfo->getUserInfo())
+        $this->hasManageAccess ??= $this->authInfo->isLoggedIn
+            && !$this->usersCtx->hasActiveBan($this->authInfo->userInfo)
             && $this->authInfo->getPerms('global')->check(Perm::G_IS_JANITOR);
         return $this->hasManageAccess;
     }
@@ -195,7 +129,7 @@ class MisuzuContext {
         $isDebug = MSZ_DEBUG;
         $globals['site_info'] = $this->siteInfo;
         $globals['auth_info'] = $this->authInfo;
-        $globals['active_ban_info'] = $this->usersCtx->tryGetActiveBan($this->authInfo->getUserInfo());
+        $globals['active_ban_info'] = $this->usersCtx->tryGetActiveBan($this->authInfo->userInfo);
         $globals['display_timings_info'] = $isDebug || $this->authInfo->getPerms('global')->check(Perm::G_TIMINGS_VIEW);
 
         $this->templating = new TplEnvironment(
@@ -210,9 +144,8 @@ class MisuzuContext {
     }
 
     public function createRouting(): RoutingContext {
-        $routingCtx = new RoutingContext();
-
-        $this->urls = $routingCtx->getUrls();
+        $routingCtx = new RoutingContext;
+        $this->urls = $routingCtx->urls;
 
         $routingCtx->register(new \Misuzu\Home\HomeRoutes(
             $this->config,
@@ -283,7 +216,7 @@ class MisuzuContext {
         $routingCtx->register(new LegacyRoutes($this->urls));
 
         $rpcServer = new HttpRpcServer;
-        $routingCtx->getRouter()->register($rpcServer->createRouteHandler(
+        $routingCtx->router->register($rpcServer->createRouteHandler(
             new HmacVerificationProvider(fn() => $this->config->getString('aleister.secret'))
         ));
 
@@ -305,7 +238,7 @@ class MisuzuContext {
 
         // This RPC server will eventually despawn when Hanyuu fully owns auth
         $hanyuuRpcServer = new HttpRpcServer;
-        $routingCtx->getRouter()->scopeTo('/_hanyuu')->register($hanyuuRpcServer->createRouteHandler(
+        $routingCtx->router->scopeTo('/_hanyuu')->register($hanyuuRpcServer->createRouteHandler(
             new HmacVerificationProvider(fn() => $this->config->getString('hanyuu.secret'))
         ));
 
diff --git a/src/News/News.php b/src/News/News.php
index 586a6427..de2b9e1b 100644
--- a/src/News/News.php
+++ b/src/News/News.php
@@ -10,11 +10,11 @@ use Misuzu\Comments\CommentsCategoryInfo;
 use Misuzu\Users\UserInfo;
 
 class News {
-    private DbConnection $dbConn;
     private DbStatementCache $cache;
 
-    public function __construct(DbConnection $dbConn) {
-        $this->dbConn = $dbConn;
+    public function __construct(
+        private DbConnection $dbConn
+    ) {
         $this->cache = new DbStatementCache($dbConn);
     }
 
@@ -84,7 +84,7 @@ class News {
         if($hasPostInfo) {
             if($postInfo instanceof NewsPostInfo) {
                 $query .= ' WHERE category_id = ?';
-                $value = $postInfo->getCategoryId();
+                $value = $postInfo->categoryId;
             } else {
                 $query .= ' WHERE category_id = (SELECT category_id FROM msz_news_posts WHERE post_id = ?)';
                 $value = $postInfo;
@@ -126,7 +126,7 @@ class News {
 
     public function deleteCategory(NewsCategoryInfo|string $infoOrId): void {
         if($infoOrId instanceof NewsCategoryInfo)
-            $infoOrId = $infoOrId->getId();
+            $infoOrId = $infoOrId->id;
 
         $stmt = $this->cache->get('DELETE FROM msz_news_categories WHERE category_id = ?');
         $stmt->addParameter(1, $infoOrId);
@@ -140,7 +140,7 @@ class News {
         ?bool $hidden = null
     ): void {
         if($infoOrId instanceof NewsCategoryInfo)
-            $infoOrId = $infoOrId->getId();
+            $infoOrId = $infoOrId->id;
 
         if($name !== null) {
             $name = trim($name);
@@ -172,7 +172,7 @@ class News {
         bool $includeDeleted = false
     ): int {
         if($categoryInfo instanceof NewsCategoryInfo)
-            $categoryInfo = $categoryInfo->getId();
+            $categoryInfo = $categoryInfo->id;
 
         $hasCategoryInfo = $categoryInfo !== null;
 
@@ -218,7 +218,7 @@ class News {
         ?Pagination $pagination = null
     ): iterable {
         if($categoryInfo instanceof NewsCategoryInfo)
-            $categoryInfo = $categoryInfo->getId();
+            $categoryInfo = $categoryInfo->id;
 
         $hasCategoryInfo = $categoryInfo !== null;
         $hasSearchQuery = $searchQuery !== null;
@@ -287,7 +287,7 @@ class News {
         DateTimeInterface|int|null $schedule = null
     ): NewsPostInfo {
         if($categoryInfo instanceof NewsCategoryInfo)
-            $categoryInfo = $categoryInfo->getId();
+            $categoryInfo = $categoryInfo->id;
         if($userInfo instanceof UserInfo)
             $userInfo = $userInfo->getId();
         if($schedule instanceof DateTimeInterface)
@@ -315,7 +315,7 @@ class News {
 
     public function deletePost(NewsPostInfo|string $postInfo): void {
         if($postInfo instanceof NewsPostInfo)
-            $postInfo = $postInfo->getId();
+            $postInfo = $postInfo->id;
 
         $stmt = $this->cache->get('UPDATE msz_news_posts SET post_deleted = COALESCE(post_deleted, NOW()) WHERE post_id = ?');
         $stmt->addParameter(1, $postInfo);
@@ -324,7 +324,7 @@ class News {
 
     public function restorePost(NewsPostInfo|string $postInfo): void {
         if($postInfo instanceof NewsPostInfo)
-            $postInfo = $postInfo->getId();
+            $postInfo = $postInfo->id;
 
         $stmt = $this->cache->get('UPDATE msz_news_posts SET post_deleted = NULL WHERE post_id = ?');
         $stmt->addParameter(1, $postInfo);
@@ -333,7 +333,7 @@ class News {
 
     public function nukePost(NewsPostInfo|string $postInfo): void {
         if($postInfo instanceof NewsPostInfo)
-            $postInfo = $postInfo->getId();
+            $postInfo = $postInfo->id;
 
         // should this enforce a soft delete first? (AND post_deleted IS NOT NULL)
         $stmt = $this->cache->get('DELETE FROM msz_news_posts WHERE post_id = ?');
@@ -352,9 +352,9 @@ class News {
         DateTimeInterface|int|null $schedule = null
     ): void {
         if($postInfo instanceof NewsPostInfo)
-            $postInfo = $postInfo->getId();
+            $postInfo = $postInfo->id;
         if($categoryInfo instanceof NewsCategoryInfo)
-            $categoryInfo = $categoryInfo->getId();
+            $categoryInfo = $categoryInfo->id;
         if($userInfo instanceof UserInfo)
             $userInfo = $userInfo->getId();
         if($schedule instanceof DateTimeInterface)
@@ -392,9 +392,9 @@ class News {
         CommentsCategoryInfo|string $commentsCategory
     ): void {
         if($postInfo instanceof NewsPostInfo)
-            $postInfo = $postInfo->getId();
+            $postInfo = $postInfo->id;
         if($commentsCategory instanceof CommentsCategoryInfo)
-            $commentsCategory = $commentsCategory->getId();
+            $commentsCategory = $commentsCategory->id;
 
         $stmt = $this->cache->get('UPDATE msz_news_posts SET comment_section_id = ? WHERE post_id = ?');
         $stmt->addParameter(1, $commentsCategory);
diff --git a/src/News/NewsCategoryInfo.php b/src/News/NewsCategoryInfo.php
index 5558bd0b..13c95035 100644
--- a/src/News/NewsCategoryInfo.php
+++ b/src/News/NewsCategoryInfo.php
@@ -6,12 +6,12 @@ use Index\Db\DbResult;
 
 class NewsCategoryInfo {
     public function __construct(
-        private string $id,
-        private string $name,
-        private string $description,
-        private bool $hidden,
-        private int $created,
-        private int $posts,
+        public private(set) string $id,
+        public private(set) string $name,
+        public private(set) string $description,
+        public private(set) bool $hidden,
+        public private(set) int $createdTime,
+        public private(set) int $postsCount,
     ) {}
 
     public static function fromResult(DbResult $result): NewsCategoryInfo {
@@ -20,36 +20,12 @@ class NewsCategoryInfo {
             name: $result->getString(1),
             description: $result->getString(2),
             hidden: $result->getBoolean(3),
-            created: $result->getInteger(4),
-            posts: $result->getInteger(5),
+            createdTime: $result->getInteger(4),
+            postsCount: $result->getInteger(5),
         );
     }
 
-    public function getId(): string {
-        return $this->id;
-    }
-
-    public function getName(): string {
-        return $this->name;
-    }
-
-    public function getDescription(): string {
-        return $this->description;
-    }
-
-    public function isHidden(): bool {
-        return $this->hidden;
-    }
-
-    public function getCreatedTime(): int {
-        return $this->created;
-    }
-
-    public function getCreatedAt(): CarbonImmutable {
-        return CarbonImmutable::createFromTimestampUTC($this->created);
-    }
-
-    public function getPostsCount(): int {
-        return $this->posts;
+    public CarbonImmutable $createdAt {
+        get => CarbonImmutable::createFromTimestampUTC($this->createdTime);
     }
 }
diff --git a/src/News/NewsPostInfo.php b/src/News/NewsPostInfo.php
index c841e6e7..e01a8bcb 100644
--- a/src/News/NewsPostInfo.php
+++ b/src/News/NewsPostInfo.php
@@ -6,17 +6,17 @@ use Index\Db\DbResult;
 
 class NewsPostInfo {
     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 private(set) string $id,
+        public private(set) string $categoryId,
+        public private(set) ?string $userId,
+        public private(set) ?string $commentsSectionId,
+        public private(set) bool $featured,
+        public private(set) string $title,
+        public private(set) string $body,
+        public private(set) int $scheduledTime,
+        public private(set) int $createdTime,
+        public private(set) int $updatedTime,
+        public private(set) ?int $deletedTime,
     ) {}
 
     public static function fromResult(DbResult $result): NewsPostInfo {
@@ -28,99 +28,49 @@ class NewsPostInfo {
             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),
+            scheduledTime: $result->getInteger(7),
+            createdTime: $result->getInteger(8),
+            updatedTime: $result->getInteger(9),
+            deletedTime: $result->getIntegerOrNull(10),
         );
     }
 
-    public function getId(): string {
-        return $this->id;
+    public string $commentsCategoryName {
+        get => sprintf('news-%s', $this->id);
     }
 
-    public function getCategoryId(): string {
-        return $this->categoryId;
+    public string $firstParagraph {
+        get {
+            $index = mb_strpos($this->body, "\n");
+            return $index === false ? $this->body : mb_substr($this->body, 0, $index);
+        }
     }
 
-    public function hasUserId(): bool {
-        return $this->userId !== null;
+    public CarbonImmutable $scheduledAt {
+        get => CarbonImmutable::createFromTimestampUTC($this->scheduledTime);
     }
 
-    public function getUserId(): ?string {
-        return $this->userId;
+    public bool $published {
+        get => $this->scheduledTime <= time();
     }
 
-    public function hasCommentsCategoryId(): bool {
-        return $this->commentsSectionId !== null;
+    public CarbonImmutable $createdAt {
+        get => CarbonImmutable::createFromTimestampUTC($this->createdTime);
     }
 
-    public function getCommentsCategoryId(): string {
-        return $this->commentsSectionId;
+    public CarbonImmutable $updatedAt {
+        get => CarbonImmutable::createFromTimestampUTC($this->updatedTime);
     }
 
-    public function getCommentsCategoryName(): string {
-        return sprintf('news-%s', $this->id);
+    public bool $edited {
+        get => $this->updatedTime > $this->createdTime;
     }
 
-    public function isFeatured(): bool {
-        return $this->featured;
+    public bool $deleted {
+        get => $this->deletedTime !== null;
     }
 
-    public function getTitle(): string {
-        return $this->title;
-    }
-
-    public function getBody(): string {
-        return $this->body;
-    }
-
-    public function getFirstParagraph(): string {
-        $index = mb_strpos($this->body, "\n");
-        return $index === false ? $this->body : mb_substr($this->body, 0, $index);
-    }
-
-    public function getScheduledTime(): int {
-        return $this->scheduled;
-    }
-
-    public function getScheduledAt(): CarbonImmutable {
-        return CarbonImmutable::createFromTimestampUTC($this->scheduled);
-    }
-
-    public function isPublished(): bool {
-        return $this->scheduled <= time();
-    }
-
-    public function getCreatedTime(): int {
-        return $this->created;
-    }
-
-    public function getCreatedAt(): CarbonImmutable {
-        return CarbonImmutable::createFromTimestampUTC($this->created);
-    }
-
-    public function getUpdatedTime(): int {
-        return $this->updated;
-    }
-
-    public function getUpdatedAt(): CarbonImmutable {
-        return CarbonImmutable::createFromTimestampUTC($this->updated);
-    }
-
-    public function isEdited(): bool {
-        return $this->updated > $this->created;
-    }
-
-    public function isDeleted(): bool {
-        return $this->deleted !== null;
-    }
-
-    public function getDeletedTime(): ?int {
-        return $this->deleted;
-    }
-
-    public function getDeletedAt(): ?CarbonImmutable {
-        return $this->deleted === null ? null : CarbonImmutable::createFromTimestampUTC($this->deleted);
+    public ?CarbonImmutable $deletedAt {
+        get => $this->deletedTime === null ? null : CarbonImmutable::createFromTimestampUTC($this->deletedTime);
     }
 }
diff --git a/src/News/NewsRoutes.php b/src/News/NewsRoutes.php
index 6f790dce..619f5246 100644
--- a/src/News/NewsRoutes.php
+++ b/src/News/NewsRoutes.php
@@ -34,16 +34,16 @@ class NewsRoutes implements RouteHandler, UrlSource {
         );
 
         foreach($postInfos as $postInfo) {
-            $categoryId = $postInfo->getCategoryId();
-            $userInfo = $postInfo->hasUserId() ? $this->usersCtx->getUserInfo($postInfo->getUserId()) : null;
+            $categoryId = $postInfo->categoryId;
+            $userInfo = $postInfo->userId !== null ? $this->usersCtx->getUserInfo($postInfo->userId) : null;
 
             if(array_key_exists($categoryId, $this->categoryInfos))
                 $categoryInfo = $this->categoryInfos[$categoryId];
             else
                 $this->categoryInfos[$categoryId] = $categoryInfo = $this->news->getCategory(postInfo: $postInfo);
 
-            $commentsCount = $postInfo->hasCommentsCategoryId()
-                ? $this->comments->countPosts(categoryInfo: $postInfo->getCommentsCategoryId(), deleted: false)
+            $commentsCount = $postInfo->commentsSectionId
+                ? $this->comments->countPosts(categoryInfo: $postInfo->commentsSectionId, deleted: false)
                 : 0;
 
             $posts[] = [
@@ -67,9 +67,8 @@ class NewsRoutes implements RouteHandler, UrlSource {
         );
 
         foreach($postInfos as $postInfo) {
-            $userId = $postInfo->getUserId();
-            $categoryId = $postInfo->getCategoryId();
-            $userInfo = $postInfo->hasUserId() ? $this->usersCtx->getUserInfo($postInfo->getUserId()) : null;
+            $categoryId = $postInfo->categoryId;
+            $userInfo = $postInfo->userId !== null ? $this->usersCtx->getUserInfo($postInfo->userId) : null;
 
             $posts[] = [
                 'post' => $postInfo,
@@ -134,22 +133,22 @@ class NewsRoutes implements RouteHandler, UrlSource {
             return 404;
         }
 
-        if(!$postInfo->isPublished() || $postInfo->isDeleted())
+        if(!$postInfo->published || $postInfo->deleted)
             return 404;
 
         $categoryInfo = $this->news->getCategory(postInfo: $postInfo);
 
-        if($postInfo->hasCommentsCategoryId())
+        if($postInfo->commentsSectionId !== null)
             try {
-                $commentsCategory = $this->comments->getCategory(categoryId: $postInfo->getCommentsCategoryId());
+                $commentsCategory = $this->comments->getCategory(categoryId: $postInfo->commentsSectionId);
             } catch(RuntimeException $ex) {}
 
         if(!isset($commentsCategory)) {
-            $commentsCategory = $this->comments->ensureCategory($postInfo->getCommentsCategoryName());
+            $commentsCategory = $this->comments->ensureCategory($postInfo->commentsCategoryName);
             $this->news->updatePostCommentCategory($postInfo, $commentsCategory);
         }
 
-        $userInfo = $postInfo->hasUserId() ? $this->usersCtx->getUserInfo($postInfo->getUserId()) : null;
+        $userInfo = $postInfo->userId !== null ? $this->usersCtx->getUserInfo($postInfo->userId) : null;
         $comments = new CommentsEx($this->authInfo, $this->comments, $this->usersCtx);
 
         return Template::renderRaw('news.post', [
@@ -168,16 +167,16 @@ class NewsRoutes implements RouteHandler, UrlSource {
         $response->setContentType('application/rss+xml; charset=utf-8');
 
         $hasCategory = $categoryInfo !== null;
-        $siteName = $this->siteInfo->getName();
-        $siteUrl = $this->siteInfo->getURL();
+        $siteName = $this->siteInfo->name;
+        $siteUrl = $this->siteInfo->url;
         $posts = $this->getNewsPostsForFeed($categoryInfo);
 
         $feed = new FeedBuilder;
         if($hasCategory) {
-            $feed->setTitle(sprintf('%s » %s', $siteName, $categoryInfo->getName()));
-            $feed->setDescription($categoryInfo->getDescription());
-            $feed->setContentUrl($siteUrl . $this->urls->format('news-category', ['category' => $categoryInfo->getId()]));
-            $feed->setFeedUrl($siteUrl . $this->urls->format('news-category-feed', ['category' => $categoryInfo->getId()]));
+            $feed->setTitle(sprintf('%s » %s', $siteName, $categoryInfo->name));
+            $feed->setDescription($categoryInfo->description);
+            $feed->setContentUrl($siteUrl . $this->urls->format('news-category', ['category' => $categoryInfo->id]));
+            $feed->setFeedUrl($siteUrl . $this->urls->format('news-category-feed', ['category' => $categoryInfo->id]));
         } else {
             $feed->setTitle(sprintf('%s » Featured News', $siteName));
             $feed->setDescription('A live featured news feed.');
@@ -191,22 +190,22 @@ class NewsRoutes implements RouteHandler, UrlSource {
                 $postInfo = $post['post'];
                 $userInfo = $post['user'];
 
-                $item->setTitle($postInfo->getTitle());
-                $item->setDescription(Parser::instance(Parser::MARKDOWN)->parseText($postInfo->getBody()));
-                $item->setCreatedAt($postInfo->getCreatedTime());
-                $item->setContentUrl($siteUrl . $this->urls->format('news-post', ['post' => $postInfo->getId()]));
-                $item->setCommentsUrl($siteUrl . $this->urls->format('news-post-comments', ['post' => $postInfo->getId()]));
+                $item->setTitle($postInfo->title);
+                $item->setDescription(Parser::instance(Parser::MARKDOWN)->parseText($postInfo->body));
+                $item->setCreatedAt($postInfo->createdTime);
+                $item->setContentUrl($siteUrl . $this->urls->format('news-post', ['post' => $postInfo->id]));
+                $item->setCommentsUrl($siteUrl . $this->urls->format('news-post-comments', ['post' => $postInfo->id]));
 
                 if($userInfo !== null) {
-                    $item->setAuthorName($userInfo->getName());
+                    $item->setAuthorName($userInfo->name);
                     $item->setAuthorUrl($siteUrl . $this->urls->format('user-profile', ['user' => $userInfo->getId()]));
                 }
 
-                $itemUpdatedAt = $postInfo->getUpdatedTime();
+                $itemUpdatedAt = $postInfo->updatedTime;
                 if($feedUpdatedAt < $itemUpdatedAt) {
                     $feed->setUpdatedAt($feedUpdatedAt = $itemUpdatedAt);
 
-                    if($postInfo->getCreatedTime() < $itemUpdatedAt)
+                    if($postInfo->createdTime < $itemUpdatedAt)
                         $item->setUpdatedAt($itemUpdatedAt);
                 }
             });
diff --git a/src/Perms/PermissionInfo.php b/src/Perms/PermissionInfo.php
index 8d8902a4..a40e2a9f 100644
--- a/src/Perms/PermissionInfo.php
+++ b/src/Perms/PermissionInfo.php
@@ -9,12 +9,12 @@ class PermissionInfo implements IPermissionResult {
     private int $calculated;
 
     public function __construct(
-        private ?string $userId,
-        private ?string $roleId,
-        private ?string $forumCategoryId,
-        private string $category,
-        private int $allow,
-        private int $deny,
+        public private(set) ?string $userId,
+        public private(set) ?string $roleId,
+        public private(set) ?string $forumCategoryId,
+        public private(set) string $category,
+        public private(set) int $allow,
+        public private(set) int $deny,
     ) {
         $this->calculated = $this->allow & ~$this->deny;
     }
@@ -30,42 +30,6 @@ class PermissionInfo implements IPermissionResult {
         );
     }
 
-    public function hasUserId(): bool {
-        return $this->userId !== null;
-    }
-
-    public function getUserId(): ?string {
-        return $this->userId;
-    }
-
-    public function hasRoleId(): bool {
-        return $this->roleId !== null;
-    }
-
-    public function getRoleId(): ?string {
-        return $this->roleId;
-    }
-
-    public function hasForumCategoryId(): bool {
-        return $this->forumCategoryId !== null;
-    }
-
-    public function getForumCategoryId(): ?string {
-        return $this->forumCategoryId;
-    }
-
-    public function getCategory(): string {
-        return $this->category;
-    }
-
-    public function getAllow(): int {
-        return $this->allow;
-    }
-
-    public function getDeny(): int {
-        return $this->deny;
-    }
-
     public function getCalculated(): int {
         return $this->calculated;
     }
diff --git a/src/Perms/Permissions.php b/src/Perms/Permissions.php
index b095791e..e0a97364 100644
--- a/src/Perms/Permissions.php
+++ b/src/Perms/Permissions.php
@@ -16,11 +16,11 @@ class Permissions {
     public const PERMS_MIN = 0;
     public const PERMS_MAX = 9007199254740991;
 
-    private DbConnection $dbConn;
     private DbStatementCache $cache;
 
-    public function __construct(DbConnection $dbConn) {
-        $this->dbConn = $dbConn;
+    public function __construct(
+        private DbConnection $dbConn
+    ) {
         $this->cache = new DbStatementCache($dbConn);
     }
 
@@ -57,7 +57,7 @@ class Permissions {
         if($hasRoleInfo)
             $stmt->addParameter(++$args, $roleInfo instanceof RoleInfo ? $roleInfo->getId() : $roleInfo);
         if($hasForumCategoryInfo)
-            $stmt->addParameter(++$args, $forumCategoryInfo instanceof ForumCategoryInfo ? $forumCategoryInfo->getId() : $forumCategoryInfo);
+            $stmt->addParameter(++$args, $forumCategoryInfo instanceof ForumCategoryInfo ? $forumCategoryInfo->id : $forumCategoryInfo);
         if($hasCategoryName) {
             if($categoryNamesIsArray) {
                 foreach($categoryNames as $name)
@@ -104,7 +104,7 @@ class Permissions {
         $stmt = $this->cache->get('INSERT INTO msz_perms (user_id, role_id, forum_id, perms_category, perms_allow, perms_deny) VALUES (?, ?, ?, ?, ?, ?)');
         $stmt->addParameter(1, $userInfo instanceof UserInfo ? $userInfo->getId() : $userInfo);
         $stmt->addParameter(2, $roleInfo instanceof RoleInfo ? $roleInfo->getId() : $roleInfo);
-        $stmt->addParameter(3, $forumCategoryInfo instanceof ForumCategoryInfo ? $forumCategoryInfo->getId() : $forumCategoryInfo);
+        $stmt->addParameter(3, $forumCategoryInfo instanceof ForumCategoryInfo ? $forumCategoryInfo->id : $forumCategoryInfo);
         $stmt->addParameter(4, $categoryName);
         $stmt->addParameter(5, $allow);
         $stmt->addParameter(6, $deny);
@@ -137,7 +137,7 @@ class Permissions {
         if($hasRoleInfo)
             $stmt->addParameter(++$args, $roleInfo instanceof RoleInfo ? $roleInfo->getId() : $roleInfo);
         if($hasForumCategoryInfo)
-            $stmt->addParameter(++$args, $forumCategoryInfo instanceof ForumCategoryInfo ? $forumCategoryInfo->getId() : $forumCategoryInfo);
+            $stmt->addParameter(++$args, $forumCategoryInfo instanceof ForumCategoryInfo ? $forumCategoryInfo->id : $forumCategoryInfo);
         if($categoryNamesIsArray) {
             foreach($categoryNames as $name)
                 $stmt->addParameter(++$args, $name);
@@ -164,7 +164,7 @@ class Permissions {
         $stmt->addParameter(++$args, $perms);
         $stmt->addParameter(++$args, $categoryName);
         if($hasForumCategoryInfo)
-            $stmt->addParameter(++$args, $forumCategoryInfo instanceof ForumCategoryInfo ? $forumCategoryInfo->getId() : $forumCategoryInfo);
+            $stmt->addParameter(++$args, $forumCategoryInfo instanceof ForumCategoryInfo ? $forumCategoryInfo->id : $forumCategoryInfo);
         if($hasUserInfo)
             $stmt->addParameter(++$args, $userInfo instanceof UserInfo ? $userInfo->getId() : $userInfo);
         $stmt->execute();
@@ -200,7 +200,7 @@ class Permissions {
         } else
             $stmt->addParameter(++$args, $categoryNames);
         if($hasForumCategoryInfo)
-            $stmt->addParameter(++$args, $forumCategoryInfo instanceof ForumCategoryInfo ? $forumCategoryInfo->getId() : $forumCategoryInfo);
+            $stmt->addParameter(++$args, $forumCategoryInfo instanceof ForumCategoryInfo ? $forumCategoryInfo->id : $forumCategoryInfo);
         if($hasUserInfo)
             $stmt->addParameter(++$args, $userInfo instanceof UserInfo ? $userInfo->getId() : $userInfo);
         $stmt->execute();
@@ -298,7 +298,7 @@ class Permissions {
     }
 
     private function precalculatePermissionsForForumCategory(DbStatement $insert, array $userIds, object $forumCat, bool $doGuest, array $catIds = []): void {
-        $catIds[] = $currentCatId = $forumCat->info->getId();
+        $catIds[] = $currentCatId = $forumCat->info->id;
         self::precalculatePermissionsLog('Precalcuting permissions for forum category #%s (%s)...', $currentCatId, implode(' <- ', $catIds));
 
         if($doGuest) {
diff --git a/src/Profile/ProfileFieldFormatInfo.php b/src/Profile/ProfileFieldFormatInfo.php
index 78ae04d2..0f7eff46 100644
--- a/src/Profile/ProfileFieldFormatInfo.php
+++ b/src/Profile/ProfileFieldFormatInfo.php
@@ -5,11 +5,11 @@ use Index\Db\DbResult;
 
 class ProfileFieldFormatInfo {
     public function __construct(
-        private string $id,
-        private string $fieldId,
-        private ?string $regex,
-        private ?string $linkFormat,
-        private string $displayFormat,
+        public private(set) string $id,
+        public private(set) string $fieldId,
+        public private(set) ?string $regex,
+        public private(set) ?string $linkFormat,
+        public private(set) string $displayFormat,
     ) {}
 
     public static function fromResult(DbResult $result): ProfileFieldFormatInfo {
@@ -22,38 +22,10 @@ class ProfileFieldFormatInfo {
         );
     }
 
-    public function getId(): string {
-        return $this->id;
-    }
-
-    public function getFieldId(): string {
-        return $this->fieldId;
-    }
-
-    public function hasRegEx(): bool {
-        return $this->regex !== null;
-    }
-
-    public function getRegEx(): ?string {
-        return $this->regex;
-    }
-
-    public function hasLinkFormat(): bool {
-        return $this->linkFormat !== null;
-    }
-
-    public function getLinkFormat(): ?string {
-        return $this->linkFormat;
-    }
-
     public function formatLink(string $value): ?string {
         return $this->linkFormat === null ? null : sprintf($this->linkFormat, $value);
     }
 
-    public function getDisplayFormat(): string {
-        return $this->displayFormat;
-    }
-
     public function formatDisplay(string $value): string {
         return sprintf($this->displayFormat, $value);
     }
diff --git a/src/Profile/ProfileFieldInfo.php b/src/Profile/ProfileFieldInfo.php
index 476abf72..a8e37895 100644
--- a/src/Profile/ProfileFieldInfo.php
+++ b/src/Profile/ProfileFieldInfo.php
@@ -5,11 +5,11 @@ use Index\Db\DbResult;
 
 class ProfileFieldInfo {
     public function __construct(
-        private string $id,
-        private int $order,
-        private string $name,
-        private string $title,
-        private string $regex,
+        public private(set) string $id,
+        public private(set) int $order,
+        public private(set) string $name,
+        public private(set) string $title,
+        public private(set) string $regex,
     ) {}
 
     public static function fromResult(DbResult $result): ProfileFieldInfo {
@@ -22,26 +22,6 @@ class ProfileFieldInfo {
         );
     }
 
-    public function getId(): string {
-        return $this->id;
-    }
-
-    public function getOrder(): int {
-        return $this->order;
-    }
-
-    public function getName(): string {
-        return $this->name;
-    }
-
-    public function getTitle(): string {
-        return $this->title;
-    }
-
-    public function getRegEx(): string {
-        return $this->regex;
-    }
-
     public function checkValue(string $value): bool {
         return preg_match($this->regex, $value) === 1;
     }
diff --git a/src/Profile/ProfileFieldValueInfo.php b/src/Profile/ProfileFieldValueInfo.php
index 2bfd418e..e24ae0a7 100644
--- a/src/Profile/ProfileFieldValueInfo.php
+++ b/src/Profile/ProfileFieldValueInfo.php
@@ -5,10 +5,10 @@ use Index\Db\DbResult;
 
 class ProfileFieldValueInfo {
     public function __construct(
-        private string $fieldId,
-        private string $userId,
-        private string $formatId,
-        private string $value,
+        public private(set) string $fieldId,
+        public private(set) string $userId,
+        public private(set) string $formatId,
+        public private(set) string $value,
     ) {}
 
     public static function fromResult(DbResult $result): ProfileFieldValueInfo {
@@ -19,20 +19,4 @@ class ProfileFieldValueInfo {
             value: $result->getString(3)
         );
     }
-
-    public function getFieldId(): string {
-        return $this->fieldId;
-    }
-
-    public function getUserId(): string {
-        return $this->userId;
-    }
-
-    public function getFormatId(): string {
-        return $this->formatId;
-    }
-
-    public function getValue(): string {
-        return $this->value;
-    }
 }
diff --git a/src/Profile/ProfileFields.php b/src/Profile/ProfileFields.php
index e948b741..b6ce01e1 100644
--- a/src/Profile/ProfileFields.php
+++ b/src/Profile/ProfileFields.php
@@ -35,7 +35,7 @@ class ProfileFields {
             foreach($fieldValueInfos as $fieldValueInfo) {
                 if(!($fieldValueInfo instanceof ProfileFieldValueInfo))
                     throw new InvalidArgumentException('All values in $fieldValueInfos must be of ProfileFieldValueInfo type.');
-                $stmt->addParameter(++$args, $fieldValueInfo->getFieldId());
+                $stmt->addParameter(++$args, $fieldValueInfo->fieldId);
             }
 
         $stmt->execute();
@@ -97,14 +97,14 @@ class ProfileFields {
             foreach($fieldInfos as $fieldInfo) {
                 if(!($fieldInfo instanceof ProfileFieldInfo))
                     throw new InvalidArgumentException('All values in $fieldInfos must be of ProfileFieldInfo type.');
-                $stmt->addParameter(++$args, $fieldInfo->getId());
+                $stmt->addParameter(++$args, $fieldInfo->id);
             }
 
         if($hasFieldValueInfos)
             foreach($fieldValueInfos as $fieldValueInfo) {
                 if(!($fieldValueInfo instanceof ProfileFieldValueInfo))
                     throw new InvalidArgumentException('All values in $fieldValueInfos must be of ProfileFieldValueInfo type.');
-                $stmt->addParameter(++$args, $fieldValueInfo->getFormatId());
+                $stmt->addParameter(++$args, $fieldValueInfo->formatId);
             }
 
         $stmt->execute();
@@ -129,7 +129,7 @@ class ProfileFields {
         string $value
     ): ProfileFieldFormatInfo {
         if($fieldInfo instanceof ProfileFieldInfo)
-            $fieldInfo = $fieldInfo->getId();
+            $fieldInfo = $fieldInfo->id;
 
         $stmt = $this->cache->get('SELECT format_id, field_id, format_regex, format_link, format_display FROM msz_profile_fields_formats WHERE field_id = ? AND (format_regex IS NULL OR ? REGEXP format_regex) ORDER BY format_regex IS NULL ASC');
         $stmt->addParameter(1, $fieldInfo);
@@ -161,7 +161,7 @@ class ProfileFields {
         UserInfo|string $userInfo
     ): ProfileFieldValueInfo {
         if($fieldInfo instanceof ProfileFieldInfo)
-            $fieldInfo = $fieldInfo->getId();
+            $fieldInfo = $fieldInfo->id;
         if($userInfo instanceof UserInfo)
             $userInfo = $userInfo->getId();
 
@@ -214,8 +214,8 @@ class ProfileFields {
                 throw new InvalidArgumentException('One of the values in $values is not correct formatted.');
 
             $rows[] = [
-                $fieldInfo->getId(),
-                $this->selectFieldFormat($fieldInfo, $value)->getId(),
+                $fieldInfo->id,
+                $this->selectFieldFormat($fieldInfo, $value)->id,
                 $value,
             ];
         }
@@ -250,7 +250,7 @@ class ProfileFields {
 
         foreach($fieldInfos as $key => $value) {
             if($value instanceof ProfileFieldInfo)
-                $fieldInfos[$key] = $value->getId();
+                $fieldInfos[$key] = $value->id;
             elseif(is_string($value))
                 throw new InvalidArgumentException('$fieldInfos array may only contain string IDs or instances of ProfileFieldInfo');
         }
diff --git a/src/RoutingContext.php b/src/RoutingContext.php
index 606f1f58..2f9049e6 100644
--- a/src/RoutingContext.php
+++ b/src/RoutingContext.php
@@ -5,8 +5,8 @@ use Index\Http\Routing\{HttpRouter,Router,RouteHandler};
 use Index\Urls\{ArrayUrlRegistry,UrlFormat,UrlRegistry,UrlSource};
 
 class RoutingContext {
-    private UrlRegistry $urls;
-    private HttpRouter $router;
+    public private(set) UrlRegistry $urls;
+    public private(set) HttpRouter $router;
 
     public function __construct() {
         $this->urls = new ArrayUrlRegistry;
@@ -14,14 +14,6 @@ class RoutingContext {
         $this->router->use('/', fn($resp) => $resp->setPoweredBy('Misuzu'));
     }
 
-    public function getUrls(): UrlRegistry {
-        return $this->urls;
-    }
-
-    public function getRouter(): Router {
-        return $this->router;
-    }
-
     public function register(RouteHandler|UrlSource $handler): void {
         if($handler instanceof RouteHandler)
             $this->router->register($handler);
diff --git a/src/Satori/SatoriRoutes.php b/src/Satori/SatoriRoutes.php
index ffad5786..a329891a 100644
--- a/src/Satori/SatoriRoutes.php
+++ b/src/Satori/SatoriRoutes.php
@@ -53,7 +53,7 @@ final class SatoriRoutes implements RouteHandler {
         }
 
         return [
-            'field_value' => $fieldValue->getValue(),
+            'field_value' => $fieldValue->value,
         ];
     }
 
@@ -68,7 +68,7 @@ final class SatoriRoutes implements RouteHandler {
         $startId = (string)$request->getParam('start', FILTER_SANITIZE_NUMBER_INT);
 
         $posts = [];
-        $postInfos = $this->forumCtx->getPosts()->getPosts(
+        $postInfos = $this->forumCtx->posts->getPosts(
             categoryInfo: $categoryIds,
             afterPostInfo: $startId,
             newerThanDays: $backlogDays,
@@ -77,22 +77,22 @@ final class SatoriRoutes implements RouteHandler {
         );
 
         foreach($postInfos as $postInfo) {
-            $topicInfo = $this->forumCtx->getTopics()->getTopic(postInfo: $postInfo);
-            $firstPostInfo = $this->forumCtx->getPosts()->getPost(topicInfo: $topicInfo);
-            $categoryInfo = $this->forumCtx->getCategories()->getCategory(topicInfo: $topicInfo);
-            $userInfo = $postInfo->hasUserId() ? $this->usersCtx->getUserInfo($postInfo->getUserId()) : null;
+            $topicInfo = $this->forumCtx->topics->getTopic(postInfo: $postInfo);
+            $firstPostInfo = $this->forumCtx->posts->getPost(topicInfo: $topicInfo);
+            $categoryInfo = $this->forumCtx->categories->getCategory(topicInfo: $topicInfo);
+            $userInfo = $postInfo->userId !== null ? $this->usersCtx->getUserInfo($postInfo->userId) : null;
             $userColour = $this->usersCtx->getUserColour($userInfo);
 
             $posts[] = [
-                'post_id' => (int)$postInfo->getId(),
-                'topic_id' => (int)$topicInfo->getId(),
-                'topic_title' => $topicInfo->getTitle(),
-                'forum_id' => (int)$categoryInfo->getId(),
-                'forum_name' => $categoryInfo->getName(),
+                'post_id' => (int)$postInfo->id,
+                'topic_id' => (int)$topicInfo->id,
+                'topic_title' => $topicInfo->title,
+                'forum_id' => (int)$categoryInfo->id,
+                'forum_name' => $categoryInfo->name,
                 'user_id' => (int)$userInfo->getId(),
-                'username' => $userInfo->getName(),
+                'username' => $userInfo->name,
                 'user_colour' => Colour::toMisuzu($userColour),
-                'is_opening_post' => $postInfo->getId() === $firstPostInfo->getId() ? 1 : 0,
+                'is_opening_post' => $postInfo->id === $firstPostInfo->id ? 1 : 0,
             ];
         }
 
@@ -105,7 +105,7 @@ final class SatoriRoutes implements RouteHandler {
         $backlogDays = $this->config->getInteger('users.backlog', 7);
         $startId = (string)$request->getParam('start', FILTER_SANITIZE_NUMBER_INT);
 
-        $userInfos = $this->usersCtx->getUsers()->getUsers(
+        $userInfos = $this->usersCtx->users->getUsers(
             after: $startId,
             newerThanDays: $backlogDays,
             orderBy: 'id',
@@ -118,7 +118,7 @@ final class SatoriRoutes implements RouteHandler {
         foreach($userInfos as $userInfo)
             $users[] = [
                 'user_id' => (int)$userInfo->getId(),
-                'username' => $userInfo->getName(),
+                'username' => $userInfo->name,
             ];
 
         return $users;
diff --git a/src/SharpChat/SharpChatRoutes.php b/src/SharpChat/SharpChatRoutes.php
index 0641778f..c19dd3cc 100644
--- a/src/SharpChat/SharpChatRoutes.php
+++ b/src/SharpChat/SharpChatRoutes.php
@@ -51,12 +51,12 @@ final class SharpChatRoutes implements RouteHandler {
             $strings = [];
 
             foreach($this->emotes->getEmoteStrings($emoteInfo) as $stringInfo)
-                $strings[] = sprintf(':%s:', $stringInfo->getString());
+                $strings[] = sprintf(':%s:', $stringInfo->string);
 
             $out[] = [
                 'Text' => $strings,
-                'Image' => $emoteInfo->getUrl(),
-                'Hierarchy' => $emoteInfo->getMinRank(),
+                'Image' => $emoteInfo->url,
+                'Hierarchy' => $emoteInfo->minRank,
             ];
         }
 
@@ -65,7 +65,7 @@ final class SharpChatRoutes implements RouteHandler {
 
     #[HttpGet('/_sockchat/login')]
     public function getLogin($response, $request) {
-        if(!$this->authInfo->isLoggedIn()) {
+        if(!$this->authInfo->isLoggedIn) {
             $response->redirect($this->urls->format('auth-login'));
             return;
         }
@@ -77,7 +77,7 @@ final class SharpChatRoutes implements RouteHandler {
     }
 
     private function canImpersonateUserId(UserInfo $impersonator, string $targetId): bool {
-        if($impersonator->isSuperUser())
+        if($impersonator->super)
             return true;
 
         $whitelist = $this->impersonateConfig->getArray(sprintf('allow.u%s', $impersonator->getId()));
@@ -109,25 +109,25 @@ final class SharpChatRoutes implements RouteHandler {
         if($request->getMethod() === 'OPTIONS')
             return 204;
 
-        $tokenInfo = $this->authInfo->getTokenInfo();
+        $tokenInfo = $this->authInfo->tokenInfo;
 
-        if(!$tokenInfo->hasSessionToken())
+        if(!$tokenInfo->hasSessionToken)
             return ['ok' => false, 'err' => 'token'];
 
         try {
-            $sessionInfo = $this->authCtx->getSessions()->getSession(sessionToken: $tokenInfo->getSessionToken());
+            $sessionInfo = $this->authCtx->sessions->getSession(sessionToken: $tokenInfo->sessionToken);
         } catch(RuntimeException $ex) {
             return ['ok' => false, 'err' => 'session'];
         }
 
-        if($sessionInfo->hasExpired())
+        if($sessionInfo->expired)
             return ['ok' => false, 'err' => 'expired'];
-        if($sessionInfo->getUserId() !== $tokenInfo->getUserId())
+        if($sessionInfo->userId !== $tokenInfo->userId)
             return ['ok' => false, 'err' => 'user'];
 
-        $userInfo = $this->usersCtx->getUsers()->getUser($sessionInfo->getUserId(), 'id');
-        $userId = $tokenInfo->hasImpersonatedUserId() && $this->canImpersonateUserId($userInfo, $tokenInfo->getImpersonatedUserId())
-            ? $tokenInfo->getImpersonatedUserId()
+        $userInfo = $this->usersCtx->users->getUser($sessionInfo->userId, 'id');
+        $userId = $tokenInfo->hasImpersonatedUserId && $this->canImpersonateUserId($userInfo, $tokenInfo->impersonatedUserId)
+            ? $tokenInfo->impersonatedUserId
             : $userInfo->getId();
 
         $tokenPacker = $this->authCtx->createAuthTokenPacker();
@@ -166,7 +166,7 @@ final class SharpChatRoutes implements RouteHandler {
             return 403;
 
         foreach($bumpList as $userId => $ipAddr)
-            $this->usersCtx->getUsers()->recordUserActivity($userId, remoteAddr: $ipAddr);
+            $this->usersCtx->users->recordUserActivity($userId, remoteAddr: $ipAddr);
     }
 
     #[HttpPost('/_sockchat/verify')]
@@ -233,41 +233,41 @@ final class SharpChatRoutes implements RouteHandler {
                 return ['success' => false, 'reason' => 'token'];
 
             try {
-                $userInfo = $this->usersCtx->getUsers()->getUser($decoded->user_id, 'id');
+                $userInfo = $this->usersCtx->users->getUser($decoded->user_id, 'id');
             } catch(RuntimeException $ex) {
                 return ['success' => false, 'reason' => 'user'];
             }
         } elseif($authMethod === 'SESS' || strcasecmp($authMethod, 'Misuzu') === 0) {
             $tokenPacker = $this->authCtx->createAuthTokenPacker();
             $tokenInfo = $tokenPacker->unpack($authToken);
-            if($tokenInfo->isEmpty()) {
+            if($tokenInfo->isEmpty) {
                 // don't support using the raw session key for Misuzu format
                 if($authMethod !== 'SESS')
                     return ['success' => false, 'reason' => 'format'];
 
                 $sessionToken = $authToken;
             } else
-                $sessionToken = $tokenInfo->getSessionToken();
+                $sessionToken = $tokenInfo->sessionToken;
 
             try {
-                $sessionInfo = $this->authCtx->getSessions()->getSession(sessionToken: $sessionToken);
+                $sessionInfo = $this->authCtx->sessions->getSession(sessionToken: $sessionToken);
             } catch(RuntimeException $ex) {
                 return ['success' => false, 'reason' => 'token'];
             }
 
-            if($sessionInfo->hasExpired()) {
-                $this->authCtx->getSessions()->deleteSessions(sessionInfos: $sessionInfo);
+            if($sessionInfo->expired) {
+                $this->authCtx->sessions->deleteSessions(sessionInfos: $sessionInfo);
                 return ['success' => false, 'reason' => 'expired'];
             }
 
-            $this->authCtx->getSessions()->recordSessionActivity(sessionInfo: $sessionInfo, remoteAddr: $ipAddress);
+            $this->authCtx->sessions->recordSessionActivity(sessionInfo: $sessionInfo, remoteAddr: $ipAddress);
 
-            $userInfo = $this->usersCtx->getUsers()->getUser($sessionInfo->getUserId(), 'id');
-            if($tokenInfo->hasImpersonatedUserId() && $this->canImpersonateUserId($userInfo, $tokenInfo->getImpersonatedUserId())) {
+            $userInfo = $this->usersCtx->users->getUser($sessionInfo->userId, 'id');
+            if($tokenInfo->hasImpersonatedUserId && $this->canImpersonateUserId($userInfo, $tokenInfo->impersonatedUserId)) {
                 $userInfoReal = $userInfo;
 
                 try {
-                    $userInfo = $this->usersCtx->getUsers()->getUser($tokenInfo->getImpersonatedUserId(), 'id');
+                    $userInfo = $this->usersCtx->users->getUser($tokenInfo->impersonatedUserId, 'id');
                 } catch(RuntimeException $ex) {
                     $userInfo = $userInfoReal;
                 }
@@ -276,20 +276,20 @@ final class SharpChatRoutes implements RouteHandler {
             return ['success' => false, 'reason' => 'unsupported'];
         }
 
-        $this->usersCtx->getUsers()->recordUserActivity($userInfo, remoteAddr: $ipAddress);
-        $userColour = $this->usersCtx->getUsers()->getUserColour($userInfo);
-        $userRank = $this->usersCtx->getUsers()->getUserRank($userInfo);
+        $this->usersCtx->users->recordUserActivity($userInfo, remoteAddr: $ipAddress);
+        $userColour = $this->usersCtx->users->getUserColour($userInfo);
+        $userRank = $this->usersCtx->users->getUserRank($userInfo);
         $chatPerms = $this->perms->getPermissions('chat', $userInfo);
 
         return [
             'success' => true,
             'user_id' => (int)$userInfo->getId(),
-            'username' => $userInfo->getName(),
+            'username' => $userInfo->name,
             'colour_raw' => Colour::toMisuzu($userColour),
             'rank' => $userRank,
             'hierarchy' => $userRank,
             'perms' => $chatPerms->getCalculated(),
-            'super' => $userInfo->isSuperUser(),
+            'super' => $userInfo->super,
         ];
     }
 
@@ -306,22 +306,22 @@ final class SharpChatRoutes implements RouteHandler {
             return 403;
 
         $list = [];
-        $bans = $this->usersCtx->getBans()->getBans(activeOnly: true);
+        $bans = $this->usersCtx->bans->getBans(activeOnly: true);
 
         foreach($bans as $banInfo) {
-            $userId = $banInfo->getUserId();
+            $userId = $banInfo->userId;
             $userInfo = $this->usersCtx->getUserInfo($userId);
             $userColour = $this->usersCtx->getUserColour($userInfo);
-            $isPerma = $banInfo->isPermanent();
+            $isPerma = $banInfo->permanent;
 
             $list[] = [
                 'is_ban' => true,
                 'user_id' => $userId,
-                'user_name' => $userInfo->getName(),
+                'user_name' => $userInfo->name,
                 'user_colour' => Colour::toMisuzu($userColour),
                 'ip_addr' => '::',
                 'is_perma' => $isPerma,
-                'expires' => date('c', $isPerma ? 0x7FFFFFFF : $banInfo->getExpiresTime())
+                'expires' => date('c', $isPerma ? 0x7FFFFFFF : $banInfo->expiresTime)
             ];
         }
 
@@ -345,7 +345,7 @@ final class SharpChatRoutes implements RouteHandler {
 
         if($userIdIsName)
             try {
-                $userInfo = $this->usersCtx->getUsers()->getUser($userId, 'name');
+                $userInfo = $this->usersCtx->users->getUser($userId, 'name');
                 $userId = (string)$userInfo->getId();
             } catch(RuntimeException $ex) {
                 $userId = '';
@@ -355,14 +355,12 @@ final class SharpChatRoutes implements RouteHandler {
         if($banInfo === null)
             return ['is_ban' => false];
 
-        $isPerma = $banInfo->isPermanent();
-
         return [
             'is_ban' => true,
-            'user_id' => $banInfo->getUserId(),
+            'user_id' => $banInfo->userId,
             'ip_addr' => '::',
-            'is_perma' => $isPerma,
-            'expires' => date('c', $isPerma ? 0x7FFFFFFF : $banInfo->getExpiresTime()),
+            'is_perma' => $banInfo->permanent,
+            'expires' => date('c', $banInfo->permanent ? 0x7FFFFFFF : $banInfo->expiresTime),
         ];
     }
 
@@ -414,19 +412,19 @@ final class SharpChatRoutes implements RouteHandler {
             $modId = 69;
 
         try {
-            $modInfo = $this->usersCtx->getUsers()->getUser($modId, 'id');
+            $modInfo = $this->usersCtx->users->getUser($modId, 'id');
         } catch(RuntimeException $ex) {
             return 404;
         }
 
         try {
-            $userInfo = $this->usersCtx->getUsers()->getUser($userId, 'id');
+            $userInfo = $this->usersCtx->users->getUser($userId, 'id');
         } catch(RuntimeException $ex) {
             return 404;
         }
 
         try {
-            $this->usersCtx->getBans()->createBan(
+            $this->usersCtx->bans->createBan(
                 $userInfo,
                 $expires,
                 $reason,
@@ -461,7 +459,7 @@ final class SharpChatRoutes implements RouteHandler {
         if($banInfo === null)
             return 404;
 
-        $this->usersCtx->getBans()->deleteBans($banInfo);
+        $this->usersCtx->bans->deleteBans($banInfo);
 
         return 204;
     }
diff --git a/src/SiteInfo.php b/src/SiteInfo.php
index f8c167a8..17afd76f 100644
--- a/src/SiteInfo.php
+++ b/src/SiteInfo.php
@@ -4,18 +4,10 @@ namespace Misuzu;
 use Index\Config\Config;
 
 class SiteInfo {
-    private bool $loaded = false;
     private array $props;
 
-    public function __construct(
-        private Config $config
-    ) {}
-
-    public function load(): void {
-        if($this->loaded)
-            return;
-        $this->loaded = true;
-        $this->props = $this->config->getValues([
+    public function __construct(Config $config) {
+        $this->props = $config->getValues([
             ['name:s', 'Misuzu'],
             'desc:s',
             'url:s',
@@ -23,31 +15,19 @@ class SiteInfo {
         ]);
     }
 
-    public function getName(): string {
-        $this->load();
-        return $this->props['name'];
+    public string $name {
+        get => $this->props['name'];
     }
 
-    public function getDescription(): string {
-        $this->load();
-        return $this->props['desc'];
+    public string $description {
+        get => $this->props['desc'];
     }
 
-    public function hasURL(): bool {
-        return $this->getURL() !== '';
+    public string $url {
+        get => rtrim($this->props['url'], '/');
     }
 
-    public function getURL(): string {
-        $this->load();
-        return rtrim($this->props['url'], '/');
-    }
-
-    public function hasExternalLogo(): bool {
-        return $this->getExternalLogo() !== '';
-    }
-
-    public function getExternalLogo(): string {
-        $this->load();
-        return $this->props['ext_logo'];
+    public string $externalLogo {
+        get => $this->props['ext_logo'];
     }
 }
diff --git a/src/TemplatingExtension.php b/src/TemplatingExtension.php
index 3146ab12..6612f551 100644
--- a/src/TemplatingExtension.php
+++ b/src/TemplatingExtension.php
@@ -30,7 +30,7 @@ final class TemplatingExtension extends AbstractExtension {
     public function getFunctions() {
         return [
             new TwigFunction('asset', $this->getAssetPath(...)),
-            new TwigFunction('url', $this->ctx->getUrls()->format(...)),
+            new TwigFunction('url', $this->ctx->urls->format(...)),
             new TwigFunction('csrf_token', CSRF::token(...)),
             new TwigFunction('git_commit_hash', GitInfo::hash(...)),
             new TwigFunction('git_tag', GitInfo::tag(...)),
@@ -63,51 +63,49 @@ final class TemplatingExtension extends AbstractExtension {
 
     public function getHeaderMenu(): array {
         $menu = [];
-        $urls = $this->ctx->getUrls();
-        $authInfo = $this->ctx->getAuthInfo();
 
         $home = [
             'title' => 'Home',
-            'url' => $urls->format('index'),
+            'url' => $this->ctx->urls->format('index'),
             'menu' => [],
         ];
 
-        if($authInfo->isLoggedIn())
+        if($this->ctx->authInfo->isLoggedIn)
             $home['menu'][] = [
                 'title' => 'Members',
-                'url' => $urls->format('user-list'),
+                'url' => $this->ctx->urls->format('user-list'),
             ];
 
         $home['menu'][] = [
             'title' => 'Changelog',
-            'url' => $urls->format('changelog-index'),
+            'url' => $this->ctx->urls->format('changelog-index'),
         ];
         $home['menu'][] = [
             'title' => 'Contact',
-            'url' => $urls->format('info', ['title' => 'contact']),
+            'url' => $this->ctx->urls->format('info', ['title' => 'contact']),
         ];
         $home['menu'][] = [
             'title' => 'Rules',
-            'url' => $urls->format('info', ['title' => 'rules']),
+            'url' => $this->ctx->urls->format('info', ['title' => 'rules']),
         ];
 
         $menu[] = $home;
 
         $menu[] = [
             'title' => 'News',
-            'url' => $urls->format('news-index'),
+            'url' => $this->ctx->urls->format('news-index'),
         ];
 
         $forum = [
             'title' => 'Forum',
-            'url' => $urls->format('forum-index'),
+            'url' => $this->ctx->urls->format('forum-index'),
             'menu' => [],
         ];
 
-        if($authInfo->getPerms('global')->check(Perm::G_FORUM_LEADERBOARD_VIEW))
+        if($this->ctx->authInfo->getPerms('global')->check(Perm::G_FORUM_LEADERBOARD_VIEW))
             $forum['menu'][] = [
                 'title' => 'Leaderboard',
-                'url' => $urls->format('forum-leaderboard'),
+                'url' => $this->ctx->urls->format('forum-leaderboard'),
             ];
 
         $menu[] = $forum;
@@ -124,68 +122,65 @@ final class TemplatingExtension extends AbstractExtension {
 
     public function getUserMenu(bool $inBroomCloset, string $manageUrl = ''): array {
         $menu = [];
-        $urls = $this->ctx->getUrls();
-        $authInfo = $this->ctx->getAuthInfo();
-        $usersCtx = $this->ctx->getUsersContext();
 
-        if($authInfo->isLoggedIn()) {
-            $userInfo = $authInfo->getUserInfo();
-            $globalPerms = $authInfo->getPerms('global');
+        if($this->ctx->authInfo->isLoggedIn) {
+            $userInfo = $this->ctx->authInfo->userInfo;
+            $globalPerms = $this->ctx->authInfo->getPerms('global');
 
             $menu[] = [
                 'title' => 'Profile',
-                'url' => $urls->format('user-profile', ['user' => $userInfo->getId()]),
+                'url' => $this->ctx->urls->format('user-profile', ['user' => $userInfo->getId()]),
                 'icon' => 'fas fa-user fa-fw',
             ];
             if($globalPerms->check(Perm::G_MESSAGES_VIEW))
                 $menu[] = [
                     'title' => 'Messages',
-                    'url' => $urls->format('messages-index'),
+                    'url' => $this->ctx->urls->format('messages-index'),
                     'icon' => 'fas fa-envelope fa-fw',
                     'class' => 'js-header-pms-button',
                 ];
             $menu[] = [
                 'title' => 'Settings',
-                'url' => $urls->format('settings-index'),
+                'url' => $this->ctx->urls->format('settings-index'),
                 'icon' => 'fas fa-cog fa-fw',
             ];
             $menu[] = [
                 'title' => 'Search',
-                'url' => $urls->format('search-index'),
+                'url' => $this->ctx->urls->format('search-index'),
                 'icon' => 'fas fa-search fa-fw',
             ];
 
-            if(!$usersCtx->hasActiveBan($userInfo) && $globalPerms->check(Perm::G_IS_JANITOR)) {
+            if(!$this->ctx->usersCtx->hasActiveBan($userInfo) && $globalPerms->check(Perm::G_IS_JANITOR)) {
                 // restore behaviour where clicking this button switches between
                 // site version and broom version
                 if($inBroomCloset)
                     $menu[] = [
                         'title' => 'Exit Broom Closet',
-                        'url' => $manageUrl === '' ? $urls->format('index') : $manageUrl,
+                        'url' => $manageUrl === '' ? $this->ctx->urls->format('index') : $manageUrl,
                         'icon' => 'fas fa-door-open fa-fw',
                     ];
                 else
                     $menu[] = [
                         'title' => 'Enter Broom Closet',
-                        'url' => $manageUrl === '' ? $urls->format('manage-index') : $manageUrl,
+                        'url' => $manageUrl === '' ? $this->ctx->urls->format('manage-index') : $manageUrl,
                         'icon' => 'fas fa-door-closed fa-fw',
                     ];
             }
 
             $menu[] = [
                 'title' => 'Log out',
-                'url' => $urls->format('auth-logout', ['csrf' => CSRF::token()]),
+                'url' => $this->ctx->urls->format('auth-logout', ['csrf' => CSRF::token()]),
                 'icon' => 'fas fa-sign-out-alt fa-fw',
             ];
         } else {
             $menu[] = [
                 'title' => 'Register',
-                'url' => $urls->format('auth-register'),
+                'url' => $this->ctx->urls->format('auth-register'),
                 'icon' => 'fas fa-user-plus fa-fw',
             ];
             $menu[] = [
                 'title' => 'Log in',
-                'url' => $urls->format('auth-login'),
+                'url' => $this->ctx->urls->format('auth-login'),
                 'icon' => 'fas fa-sign-in-alt fa-fw',
             ];
         }
@@ -194,51 +189,49 @@ final class TemplatingExtension extends AbstractExtension {
     }
 
     public function getManageMenu(): array {
-        $urls = $this->ctx->getUrls();
-        $authInfo = $this->ctx->getAuthInfo();
-        $globalPerms = $authInfo->getPerms('global');
-        if(!$authInfo->isLoggedIn() || !$globalPerms->check(Perm::G_IS_JANITOR))
+        $globalPerms = $this->ctx->authInfo->getPerms('global');
+        if(!$this->ctx->authInfo->isLoggedIn || !$globalPerms->check(Perm::G_IS_JANITOR))
             return [];
 
         $menu = [
             'General' => [
-                'Overview' => $urls->format('manage-general-overview'),
+                'Overview' => $this->ctx->urls->format('manage-general-overview'),
             ],
         ];
 
         if($globalPerms->check(Perm::G_LOGS_VIEW))
-            $menu['General']['Logs'] = $urls->format('manage-general-logs');
+            $menu['General']['Logs'] = $this->ctx->urls->format('manage-general-logs');
         if($globalPerms->check(Perm::G_EMOTES_MANAGE))
-            $menu['General']['Emoticons'] = $urls->format('manage-general-emoticons');
+            $menu['General']['Emoticons'] = $this->ctx->urls->format('manage-general-emoticons');
         if($globalPerms->check(Perm::G_CONFIG_MANAGE))
-            $menu['General']['Settings'] = $urls->format('manage-general-settings');
+            $menu['General']['Settings'] = $this->ctx->urls->format('manage-general-settings');
 
-        $userPerms = $authInfo->getPerms('user');
+        $userPerms = $this->ctx->authInfo->getPerms('user');
         if($userPerms->check(Perm::U_USERS_MANAGE))
-            $menu['Users & Roles']['Users'] = $urls->format('manage-users');
+            $menu['Users & Roles']['Users'] = $this->ctx->urls->format('manage-users');
         if($userPerms->check(Perm::U_ROLES_MANAGE))
-            $menu['Users & Roles']['Roles'] = $urls->format('manage-roles');
+            $menu['Users & Roles']['Roles'] = $this->ctx->urls->format('manage-roles');
         if($userPerms->check(Perm::U_NOTES_MANAGE))
-            $menu['Users & Roles']['Notes'] = $urls->format('manage-users-notes');
+            $menu['Users & Roles']['Notes'] = $this->ctx->urls->format('manage-users-notes');
         if($userPerms->check(Perm::U_WARNINGS_MANAGE))
-            $menu['Users & Roles']['Warnings'] = $urls->format('manage-users-warnings');
+            $menu['Users & Roles']['Warnings'] = $this->ctx->urls->format('manage-users-warnings');
         if($userPerms->check(Perm::U_BANS_MANAGE))
-            $menu['Users & Roles']['Bans'] = $urls->format('manage-users-bans');
+            $menu['Users & Roles']['Bans'] = $this->ctx->urls->format('manage-users-bans');
 
         if($globalPerms->check(Perm::G_NEWS_POSTS_MANAGE))
-            $menu['News']['Posts'] = $urls->format('manage-news-posts');
+            $menu['News']['Posts'] = $this->ctx->urls->format('manage-news-posts');
         if($globalPerms->check(Perm::G_NEWS_CATEGORIES_MANAGE))
-            $menu['News']['Categories'] = $urls->format('manage-news-categories');
+            $menu['News']['Categories'] = $this->ctx->urls->format('manage-news-categories');
 
         if($globalPerms->check(Perm::G_FORUM_CATEGORIES_MANAGE))
-            $menu['Forum']['Permission Calculator'] = $urls->format('manage-forum-categories');
+            $menu['Forum']['Permission Calculator'] = $this->ctx->urls->format('manage-forum-categories');
         if($globalPerms->check(Perm::G_FORUM_TOPIC_REDIRS_MANAGE))
-            $menu['Forum']['Topic Redirects'] = $urls->format('manage-forum-topic-redirs');
+            $menu['Forum']['Topic Redirects'] = $this->ctx->urls->format('manage-forum-topic-redirs');
 
         if($globalPerms->check(Perm::G_CL_CHANGES_MANAGE))
-            $menu['Changelog']['Changes'] = $urls->format('manage-changelog-changes');
+            $menu['Changelog']['Changes'] = $this->ctx->urls->format('manage-changelog-changes');
         if($globalPerms->check(Perm::G_CL_TAGS_MANAGE))
-            $menu['Changelog']['Tags'] = $urls->format('manage-changelog-tags');
+            $menu['Changelog']['Tags'] = $this->ctx->urls->format('manage-changelog-tags');
 
         return $menu;
     }
diff --git a/src/Users/Assets/AssetsRoutes.php b/src/Users/Assets/AssetsRoutes.php
index a2047e61..b9944fd5 100644
--- a/src/Users/Assets/AssetsRoutes.php
+++ b/src/Users/Assets/AssetsRoutes.php
@@ -19,7 +19,7 @@ class AssetsRoutes implements RouteHandler, UrlSource {
     ) {}
 
     private function canViewAsset($request, UserInfo $assetUser): bool {
-        if($this->usersCtx->getBans()->countActiveBans($assetUser))
+        if($this->usersCtx->bans->countActiveBans($assetUser))
             // allow staff viewing profile to still see banned user assets
             // should change the Referer check with some query param only applied when needed
             return $this->authInfo->getPerms('user')->check(Perm::U_USERS_MANAGE)
diff --git a/src/Users/Assets/UserBackgroundAsset.php b/src/Users/Assets/UserBackgroundAsset.php
index fac6a1e6..774a0b9a 100644
--- a/src/Users/Assets/UserBackgroundAsset.php
+++ b/src/Users/Assets/UserBackgroundAsset.php
@@ -50,7 +50,7 @@ class UserBackgroundAsset extends UserImageAsset {
 
     public function __construct(UserInfo $userInfo) {
         parent::__construct($userInfo);
-        $this->settings = (int)$userInfo->getBackgroundSettings();
+        $this->settings = (int)$userInfo->backgroundSettings;
     }
 
     public function getSettings(): int {
diff --git a/src/Users/BanInfo.php b/src/Users/BanInfo.php
index 7d64e5c2..165e6b00 100644
--- a/src/Users/BanInfo.php
+++ b/src/Users/BanInfo.php
@@ -6,14 +6,14 @@ use Index\Db\DbResult;
 
 class BanInfo {
     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 private(set) string $id,
+        public private(set) string $userId,
+        public private(set) ?string $modId,
+        public private(set) int $severity,
+        public private(set) string $publicReason,
+        public private(set) string $privateReason,
+        public private(set) int $createdTime,
+        public private(set) ?int $expiresTime,
     ) {}
 
     public static function fromResult(DbResult $result): BanInfo {
@@ -24,73 +24,29 @@ class BanInfo {
             severity: $result->getInteger(3),
             publicReason: $result->getString(4),
             privateReason: $result->getString(5),
-            created: $result->getInteger(6),
-            expires: $result->getIntegerOrNull(7),
+            createdTime: $result->getInteger(6),
+            expiresTime: $result->getIntegerOrNull(7),
         );
     }
 
-    public function getId(): string {
-        return $this->id;
+    public CarbonImmutable $createdAt {
+        get => CarbonImmutable::createFromTimestampUTC($this->createdTime);
     }
 
-    public function getUserId(): string {
-        return $this->userId;
+    public bool $permanent {
+        get => $this->expiresTime === null;
     }
 
-    public function hasModId(): bool {
-        return $this->modId !== null;
+    public ?CarbonImmutable $expiresAt {
+        get => $this->expiresTime === null ? null : CarbonImmutable::createFromTimestampUTC($this->expiresTime);
     }
 
-    public function getModId(): ?string {
-        return $this->modId;
+    public bool $active {
+        get => $this->expiresTime === null || $this->expiresTime > time();
     }
 
-    public function getSeverity(): int {
-        return $this->severity;
-    }
-
-    public function hasPublicReason(): bool {
-        return $this->publicReason !== '';
-    }
-
-    public function getPublicReason(): string {
-        return $this->publicReason;
-    }
-
-    public function hasPrivateReason(): bool {
-        return $this->privateReason !== '';
-    }
-
-    public function getPrivateReason(): string {
-        return $this->privateReason;
-    }
-
-    public function getCreatedTime(): int {
-        return $this->created;
-    }
-
-    public function getCreatedAt(): CarbonImmutable {
-        return CarbonImmutable::createFromTimestampUTC($this->created);
-    }
-
-    public function isPermanent(): bool {
-        return $this->expires === null;
-    }
-
-    public function getExpiresTime(): ?int {
-        return $this->expires;
-    }
-
-    public function getExpiresAt(): ?CarbonImmutable {
-        return $this->expires === null ? null : CarbonImmutable::createFromTimestampUTC($this->expires);
-    }
-
-    public function isActive(): bool {
-        return $this->expires === null || $this->expires > time();
-    }
-
-    public function isExpired(): bool {
-        return $this->expires !== null && $this->expires <= time();
+    public bool $expired {
+        get => $this->expiresTime !== null && $this->expiresTime <= time();
     }
 
     private const DURATION_DIVS = [
@@ -117,11 +73,11 @@ class BanInfo {
         return 'an amount of time';
     }
 
-    public function getDurationString(): string {
-        return self::getTimeString($this->expires, $this->created);
+    public string $durationString {
+        get => self::getTimeString($this->expiresTime, $this->createdTime);
     }
 
-    public function getRemainingString(): string {
-        return self::getTimeString($this->expires, time());
+    public string $remainingString {
+        get => self::getTimeString($this->expiresTime, time());
     }
 }
diff --git a/src/Users/Bans.php b/src/Users/Bans.php
index de5b9818..252ed090 100644
--- a/src/Users/Bans.php
+++ b/src/Users/Bans.php
@@ -191,7 +191,7 @@ class Bans {
         $args = 0;
         foreach($banInfos as $banInfo) {
             if($banInfo instanceof BanInfo)
-                $banInfo = $banInfo->getId();
+                $banInfo = $banInfo->id;
             elseif(!is_string($banInfo))
                 throw new InvalidArgumentException('$banInfos must be strings of instances of BanInfo.');
 
diff --git a/src/Users/ModNoteInfo.php b/src/Users/ModNoteInfo.php
index 5b1c28a9..c8e32ead 100644
--- a/src/Users/ModNoteInfo.php
+++ b/src/Users/ModNoteInfo.php
@@ -6,67 +6,37 @@ use Index\Db\DbResult;
 
 class ModNoteInfo {
     public function __construct(
-        private string $noteId,
-        private string $userId,
-        private ?string $authorId,
-        private int $created,
-        private string $title,
-        private string $body,
+        public private(set) string $id,
+        public private(set) string $userId,
+        public private(set) ?string $authorId,
+        public private(set) int $createdTime,
+        public private(set) string $title,
+        public private(set) string $body,
     ) {}
 
     public static function fromResult(DbResult $result): ModNoteInfo {
         return new ModNoteInfo(
-            noteId: $result->getString(0),
+            id: $result->getString(0),
             userId: $result->getString(1),
             authorId: $result->getStringOrNull(2),
-            created: $result->getInteger(3),
+            createdTime: $result->getInteger(3),
             title: $result->getString(4),
             body: $result->getString(5)
         );
     }
 
-    public function getId(): string {
-        return $this->noteId;
+    public CarbonImmutable $createdAt {
+        get => CarbonImmutable::createFromTimestampUTC($this->createdTime);
     }
 
-    public function getUserId(): string {
-        return $this->userId;
+    public string $firstParagraph {
+        get {
+            $index = mb_strpos($this->body, "\n");
+            return $index === false ? $this->body : mb_substr($this->body, 0, $index);
+        }
     }
 
-    public function hasAuthorId(): bool {
-        return $this->authorId !== null;
-    }
-
-    public function getAuthorId(): ?string {
-        return $this->authorId;
-    }
-
-    public function getCreatedTime(): int {
-        return $this->created;
-    }
-
-    public function getCreatedAt(): CarbonImmutable {
-        return CarbonImmutable::createFromTimestampUTC($this->created);
-    }
-
-    public function getTitle(): string {
-        return $this->title;
-    }
-
-    public function hasBody(): bool {
-        return trim($this->body) !== '';
-    }
-
-    public function getBody(): string {
-        return $this->body;
-    }
-
-    public function getFirstParagraph(): string {
-        $index = mb_strpos($this->body, "\n");
-        return $index === false ? $this->body : mb_substr($this->body, 0, $index);
-    }
-
-    public function hasMoreParagraphs(): bool {
-        return mb_strpos($this->body, "\n") !== false;
+    public bool $hasMoreParagraphs {
+        get => mb_strpos($this->body, "\n") !== false;
     }
 }
diff --git a/src/Users/ModNotes.php b/src/Users/ModNotes.php
index 7479483d..1a751b0d 100644
--- a/src/Users/ModNotes.php
+++ b/src/Users/ModNotes.php
@@ -7,11 +7,11 @@ use Index\Db\{DbConnection,DbStatementCache,DbTools};
 use Misuzu\Pagination;
 
 class ModNotes {
-    private DbConnection $dbConn;
     private DbStatementCache $cache;
 
-    public function __construct(DbConnection $dbConn) {
-        $this->dbConn = $dbConn;
+    public function __construct(
+        private DbConnection $dbConn
+    ) {
         $this->cache = new DbStatementCache($dbConn);
     }
 
@@ -141,7 +141,7 @@ class ModNotes {
         $args = 0;
         foreach($noteInfos as $noteInfo) {
             if($noteInfo instanceof ModNoteInfo)
-                $noteInfo = $noteInfo->getId();
+                $noteInfo = $noteInfo->id;
             elseif(!is_string($noteInfo))
                 throw new InvalidArgumentException('$noteInfos must be strings of instances of ModNoteInfo.');
 
@@ -157,7 +157,7 @@ class ModNotes {
         ?string $body = null
     ): void {
         if($noteInfo instanceof ModNoteInfo)
-            $noteInfo = $noteInfo->getId();
+            $noteInfo = $noteInfo->id;
 
         $stmt = $this->cache->get('UPDATE msz_users_modnotes SET note_title = COALESCE(?, note_title), note_body = COALESCE(?, note_body) WHERE note_id = ?');
         $stmt->addParameter(1, $title);
diff --git a/src/Users/RoleInfo.php b/src/Users/RoleInfo.php
index b4700045..c4bebbdf 100644
--- a/src/Users/RoleInfo.php
+++ b/src/Users/RoleInfo.php
@@ -8,16 +8,16 @@ use Index\Db\DbResult;
 
 class RoleInfo implements Stringable {
     public function __construct(
-        private string $id,
-        private ?string $string,
-        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 private(set) string $id,
+        public private(set) ?string $string,
+        public private(set) int $rank,
+        public private(set) string $name,
+        public private(set) ?string $title,
+        public private(set) ?string $description,
+        public private(set) bool $hidden,
+        public private(set) bool $leavable,
+        public private(set) ?int $colourRaw,
+        public private(set) int $createdTime,
     ) {}
 
     public static function fromResult(DbResult $result): RoleInfo {
@@ -30,80 +30,29 @@ class RoleInfo implements Stringable {
             description: $result->getStringOrNull(5),
             hidden: $result->getBoolean(6),
             leavable: $result->getBoolean(7),
-            colour: $result->getIntegerOrNull(8),
-            created: $result->getInteger(9),
+            colourRaw: $result->getIntegerOrNull(8),
+            createdTime: $result->getInteger(9),
         );
     }
 
-    public function getId(): string {
-        return $this->id;
+    public bool $default {
+        get => $this->id === Roles::DEFAULT_ROLE;
     }
 
-    public function isDefault(): bool {
-        return $this->id === Roles::DEFAULT_ROLE;
+    public bool $hasColour {
+        get => $this->colourRaw !== null && ($this->colourRaw & 0x40000000) === 0;
     }
 
-    public function hasString(): bool {
-        return $this->string !== null && $this->string !== '';
+    public Colour $colour {
+        get => $this->colourRaw === null ? Colour::none() : Colour::fromMisuzu($this->colourRaw);
     }
 
-    public function getString(): ?string {
-        return $this->string;
-    }
-
-    public function getRank(): int {
-        return $this->rank;
-    }
-
-    public function getName(): string {
-        return $this->name;
-    }
-
-    public function hasTitle(): bool {
-        return $this->title !== null && $this->title !== '';
-    }
-
-    public function getTitle(): ?string {
-        return $this->title;
-    }
-
-    public function hasDescription(): bool {
-        return $this->description !== null && $this->description !== '';
-    }
-
-    public function getDescription(): ?string {
-        return $this->description;
-    }
-
-    public function isHidden(): bool {
-        return $this->hidden;
-    }
-
-    public function isLeavable(): bool {
-        return $this->leavable;
-    }
-
-    public function hasColour(): bool {
-        return $this->colour !== null && ($this->colour & 0x40000000) === 0;
-    }
-
-    public function getColourRaw(): ?int {
-        return $this->colour;
-    }
-
-    public function getColour(): Colour {
-        return $this->colour === null ? Colour::none() : Colour::fromMisuzu($this->colour);
-    }
-
-    public function getCreatedTime(): int {
-        return $this->created;
-    }
-
-    public function getCreatedAt(): CarbonImmutable {
-        return CarbonImmutable::createFromTimestampUTC($this->created);
+    public CarbonImmutable $createdAt {
+        get => CarbonImmutable::createFromTimestampUTC($this->createdTime);
     }
 
     public function __toString(): string {
+        // I HAVE NO CLUE if this is actually used or referenced anywhere
         return 'r' . $this->id;
     }
 }
diff --git a/src/Users/Roles.php b/src/Users/Roles.php
index dcba4669..042da681 100644
--- a/src/Users/Roles.php
+++ b/src/Users/Roles.php
@@ -152,7 +152,7 @@ class Roles {
         $args = 0;
         foreach($roleInfos as $roleInfo) {
             if($roleInfo instanceof RoleInfo)
-                $roleInfo = $roleInfo->getId();
+                $roleInfo = $roleInfo->id;
             elseif(!is_string($roleInfo))
                 throw new InvalidArgumentException('$roleInfos must be strings of instances of RoleInfo.');
 
@@ -174,7 +174,7 @@ class Roles {
         ?bool $leavable = null
     ): void {
         if($roleInfo instanceof RoleInfo)
-            $roleInfo = $roleInfo->getId();
+            $roleInfo = $roleInfo->id;
 
         $applyString = $string !== null;
         $applyTitle = $title !== null;
@@ -218,7 +218,7 @@ class Roles {
 
     public function countRoleUsers(RoleInfo|string $roleInfo): int {
         if($roleInfo instanceof RoleInfo)
-            $roleInfo = $roleInfo->getId();
+            $roleInfo = $roleInfo->id;
 
         $stmt = $this->cache->get('SELECT COUNT(*) FROM msz_users_roles WHERE role_id = ?');
         $stmt->addParameter(1, $roleInfo);
diff --git a/src/Users/UserInfo.php b/src/Users/UserInfo.php
index 1644b6d8..42982172 100644
--- a/src/Users/UserInfo.php
+++ b/src/Users/UserInfo.php
@@ -8,27 +8,27 @@ use Index\Db\DbResult;
 
 class UserInfo {
     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 private(set) string $id,
+        public private(set) string $name,
+        #[\SensitiveParameter] private ?string $passwordHash,
+        #[\SensitiveParameter] public private(set) string $emailAddress,
+        public private(set) string $registerRemoteAddress,
+        public private(set) string $lastRemoteAddress,
+        public private(set) bool $super,
+        public private(set) string $countryCode,
+        public private(set) ?int $colourRaw,
+        public private(set) int $createdTime,
+        public private(set) ?int $lastActiveTime,
+        public private(set) ?int $deletedTime,
+        public private(set) ?string $displayRoleId,
+        #[\SensitiveParameter] public private(set) ?string $totpKey,
+        public private(set) ?string $aboutBody,
+        public private(set) int $aboutBodyParser,
+        public private(set) ?string $signatureBody,
+        public private(set) int $signatureBodyParser,
+        public private(set) ?string $birthdateRaw,
+        public private(set) ?int $backgroundSettings,
+        public private(set) ?string $title,
     ) {}
 
     public static function fromResult(DbResult $result): self {
@@ -36,22 +36,22 @@ class 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),
+            emailAddress: $result->getString(3),
+            registerRemoteAddress: $result->getString(4),
+            lastRemoteAddress: $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),
+            colourRaw: $result->getIntegerOrNull(8),
+            createdTime: $result->getInteger(9),
+            lastActiveTime: $result->getIntegerOrNull(10),
+            deletedTime: $result->getIntegerOrNull(11),
             displayRoleId: $result->getStringOrNull(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),
+            aboutBody: $result->getStringOrNull(14),
+            aboutBodyParser: $result->getInteger(15),
+            signatureBody: $result->getStringOrNull(16),
+            signatureBodyParser: $result->getInteger(17),
+            birthdateRaw: $result->getStringOrNull(18),
             backgroundSettings: $result->getIntegerOrNull(19),
             title: $result->getString(20),
         );
@@ -61,190 +61,84 @@ class UserInfo {
         return $this->id;
     }
 
-    public function getName(): string {
-        return $this->name;
+    public bool $hasPasswordHash {
+        get => $this->passwordHash !== null && $this->passwordHash !== '';
     }
 
-    public function hasPasswordHash(): bool {
-        return $this->passwordHash !== null && $this->passwordHash !== '';
-    }
-
-    public function getPasswordHash(): ?string {
-        return $this->passwordHash;
-    }
-
-    public function passwordNeedsRehash(): bool {
-        return $this->hasPasswordHash() && Users::passwordNeedsRehash($this->passwordHash);
+    public bool $passwordNeedsRehash {
+        get => $this->hasPasswordHash && Users::passwordNeedsRehash($this->passwordHash);
     }
 
     public function verifyPassword(string $password): bool {
-        return $this->hasPasswordHash() && password_verify($password, $this->passwordHash);
+        return $this->hasPasswordHash && password_verify($password, $this->passwordHash);
     }
 
-    public function getEMailAddress(): string {
-        return $this->emailAddr;
+    public bool $hasColour {
+        get => $this->colourRaw !== null && ($this->colourRaw & 0x40000000) === 0;
     }
 
-    public function getRegisterRemoteAddress(): string {
-        return $this->registerRemoteAddr;
+    public Colour $colour {
+        get => $this->colourRaw === null ? Colour::none() : Colour::fromMisuzu($this->colourRaw);
     }
 
-    public function getLastRemoteAddress(): string {
-        return $this->lastRemoteAddr;
+    public CarbonImmutable $createdAt {
+        get => CarbonImmutable::createFromTimestampUTC($this->createdTime);
     }
 
-    public function isSuperUser(): bool {
-        return $this->super;
+    public ?CarbonImmutable $lastActiveAt {
+        get => $this->lastActiveTime === null ? null : CarbonImmutable::createFromTimestampUTC($this->lastActiveTime);
     }
 
-    public function hasCountryCode(): bool {
-        return $this->countryCode !== 'XX';
+    public bool $deleted {
+        get => $this->deletedTime !== null;
     }
 
-    public function getCountryCode(): string {
-        return $this->countryCode;
+    public ?CarbonImmutable $deletedAt {
+        get => $this->deletedTime === null ? null : CarbonImmutable::createFromTimestampUTC($this->deletedTime);
     }
 
-    public function hasColour(): bool {
-        return $this->colour !== null && ($this->colour & 0x40000000) === 0;
+    public bool $hasTOTP {
+        get => $this->totpKey !== null;
     }
 
-    public function getColourRaw(): ?int {
-        return $this->colour;
+    public bool $isAboutBodyPlain {
+        get => $this->aboutBodyParser === Parser::PLAIN;
     }
 
-    public function getColour(): Colour {
-        return $this->colour === null ? Colour::none() : Colour::fromMisuzu($this->colour);
+    public bool $isAboutBodyBBCode {
+        get => $this->aboutBodyParser === Parser::BBCODE;
     }
 
-    public function getCreatedTime(): int {
-        return $this->created;
+    public bool $isAboutBodyMarkdown {
+        get => $this->aboutBodyParser === Parser::MARKDOWN;
     }
 
-    public function getCreatedAt(): CarbonImmutable {
-        return CarbonImmutable::createFromTimestampUTC($this->created);
+    public bool $isSignatureBodyPlain {
+        get => $this->signatureBodyParser === Parser::PLAIN;
     }
 
-    public function hasLastActive(): bool {
-        return $this->lastActive !== null;
+    public bool $isSignatureBodyBBCode {
+        get => $this->signatureBodyParser === Parser::BBCODE;
     }
 
-    public function getLastActiveTime(): ?int {
-        return $this->lastActive;
+    public bool $isSignatureBodyMarkdown {
+        get => $this->signatureBodyParser === Parser::MARKDOWN;
     }
 
-    public function getLastActiveAt(): ?CarbonImmutable {
-        return $this->lastActive === null ? null : CarbonImmutable::createFromTimestampUTC($this->lastActive);
+    public bool $hasBirthdate {
+        get => $this->birthdateRaw !== null;
     }
 
-    public function isDeleted(): bool {
-        return $this->deleted !== null;
+    public ?CarbonImmutable $birthdate {
+        get => $this->birthdateRaw === null ? null : CarbonImmutable::createFromFormat('Y-m-d', $this->birthdateRaw, 'UTC');
     }
 
-    public function getDeletedTime(): ?int {
-        return $this->deleted;
-    }
-
-    public function getDeletedAt(): ?CarbonImmutable {
-        return $this->deleted === null ? null : CarbonImmutable::createFromTimestampUTC($this->deleted);
-    }
-
-    public function hasDisplayRoleId(): bool {
-        return $this->displayRoleId !== null;
-    }
-
-    public function getDisplayRoleId(): ?string {
-        return $this->displayRoleId;
-    }
-
-    public function hasTOTPKey(): bool {
-        return $this->totpKey !== null;
-    }
-
-    public function getTOTPKey(): ?string {
-        return $this->totpKey;
-    }
-
-    public function hasAboutContent(): bool {
-        return $this->aboutContent !== null && $this->aboutContent !== '';
-    }
-
-    public function getAboutContent(): ?string {
-        return $this->aboutContent;
-    }
-
-    public function getAboutParser(): int {
-        return $this->aboutParser;
-    }
-
-    public function isAboutBodyPlain(): bool {
-        return $this->aboutParser === Parser::PLAIN;
-    }
-
-    public function isAboutBodyBBCode(): bool {
-        return $this->aboutParser === Parser::BBCODE;
-    }
-
-    public function isAboutBodyMarkdown(): bool {
-        return $this->aboutParser === Parser::MARKDOWN;
-    }
-
-    public function hasSignatureContent(): bool {
-        return $this->signatureContent !== null && $this->signatureContent !== '';
-    }
-
-    public function getSignatureContent(): ?string {
-        return $this->signatureContent;
-    }
-
-    public function getSignatureParser(): int {
-        return $this->signatureParser;
-    }
-
-    public function isSignatureBodyPlain(): bool {
-        return $this->signatureParser === Parser::PLAIN;
-    }
-
-    public function isSignatureBodyBBCode(): bool {
-        return $this->signatureParser === Parser::BBCODE;
-    }
-
-    public function isSignatureBodyMarkdown(): bool {
-        return $this->signatureParser === Parser::MARKDOWN;
-    }
-
-    public function hasBirthdate(): bool {
-        return $this->birthdate !== null;
-    }
-
-    public function getBirthdateRaw(): ?string {
-        return $this->birthdate;
-    }
-
-    public function getBirthdate(): ?CarbonImmutable {
-        return $this->birthdate === null ? null : CarbonImmutable::createFromFormat('Y-m-d', $this->birthdate, 'UTC');
-    }
-
-    public function getAge(): int {
-        $birthdate = $this->getBirthdate();
-        if($birthdate === null || (int)$birthdate->format('Y') < 1900)
-            return -1;
-        return (int)$birthdate->diff(CarbonImmutable::now())->format('%y');
-    }
-
-    public function hasBackgroundSettings(): bool {
-        return $this->backgroundSettings !== null;
-    }
-
-    public function getBackgroundSettings(): ?int {
-        return $this->backgroundSettings;
-    }
-
-    public function hasTitle(): bool {
-        return $this->title !== null && $this->title !== '';
-    }
-
-    public function getTitle(): ?string {
-        return $this->title;
+    public int $age {
+        get {
+            $birthdate = $this->birthdate;
+            if($birthdate === null || (int)$birthdate->format('Y') < 1900)
+                return -1;
+            return (int)$birthdate->diff(CarbonImmutable::now())->format('%y');
+        }
     }
 }
diff --git a/src/Users/Users.php b/src/Users/Users.php
index 45787ab8..5eba2b16 100644
--- a/src/Users/Users.php
+++ b/src/Users/Users.php
@@ -319,10 +319,10 @@ class Users {
         ?Colour $colour = null,
         RoleInfo|string|null $displayRoleInfo = null,
         ?string $totpKey = null,
-        ?string $aboutContent = null,
-        ?int $aboutParser = null,
-        ?string $signatureContent = null,
-        ?int $signatureParser = null,
+        ?string $aboutBody = null,
+        ?int $aboutBodyParser = null,
+        ?string $signatureBody = null,
+        ?int $signatureBodyParser = null,
         ?int $birthYear = null,
         ?int $birthMonth = null,
         ?int $birthDay = null,
@@ -378,24 +378,24 @@ class Users {
             $values[] = $totpKey === '' ? null : $totpKey;
         }
 
-        if($aboutContent !== null && $aboutParser !== null) {
-            if(self::validateProfileAbout($aboutParser, $aboutContent) !== '')
-                throw new InvalidArgumentException('$aboutContent and $aboutParser contain invalid data!');
+        if($aboutBody !== null && $aboutBodyParser !== null) {
+            if(self::validateProfileAbout($aboutBodyParser, $aboutBody) !== '')
+                throw new InvalidArgumentException('$aboutBody and $aboutBodyParser contain invalid data!');
 
             $fields[] = 'user_about_content = ?';
-            $values[] = $aboutContent;
+            $values[] = $aboutBody;
             $fields[] = 'user_about_parser = ?';
-            $values[] = $aboutParser;
+            $values[] = $aboutBodyParser;
         }
 
-        if($signatureContent !== null && $signatureParser !== null) {
-            if(self::validateForumSignature($signatureParser, $signatureContent) !== '')
-                throw new InvalidArgumentException('$signatureContent and $signatureParser contain invalid data!');
+        if($signatureBody !== null && $signatureBodyParser !== null) {
+            if(self::validateForumSignature($signatureBodyParser, $signatureBody) !== '')
+                throw new InvalidArgumentException('$signatureBody and $signatureBodyParser contain invalid data!');
 
             $fields[] = 'user_signature_content = ?';
-            $values[] = $signatureContent;
+            $values[] = $signatureBody;
             $fields[] = 'user_signature_parser = ?';
-            $values[] = $signatureParser;
+            $values[] = $signatureBodyParser;
         }
 
         if($birthMonth !== null && $birthDay !== null) {
@@ -559,11 +559,11 @@ class Users {
 
     public function getUserColour(UserInfo|string $userInfo): Colour {
         if($userInfo instanceof UserInfo) {
-            if($userInfo->hasColour())
-                return $userInfo->getColour();
+            if($userInfo->hasColour)
+                return $userInfo->colour;
 
             $query = '?';
-            $value = $userInfo->getDisplayRoleId();
+            $value = $userInfo->displayRoleId;
         } else {
             $query = '(SELECT display_role FROM msz_users WHERE user_id = ?)';
             $value = $userInfo;
diff --git a/src/Users/UsersContext.php b/src/Users/UsersContext.php
index b051ffb4..fd7e6e5a 100644
--- a/src/Users/UsersContext.php
+++ b/src/Users/UsersContext.php
@@ -5,11 +5,11 @@ use Index\Colour\Colour;
 use Index\Db\DbConnection;
 
 class UsersContext {
-    private Users $users;
-    private Roles $roles;
-    private Bans $bans;
-    private Warnings $warnings;
-    private ModNotes $modNotes;
+    public private(set) Users $users;
+    public private(set) Roles $roles;
+    public private(set) Bans $bans;
+    public private(set) Warnings $warnings;
+    public private(set) ModNotes $modNotes;
 
     private array $userInfos = [];
     private array $userColours = [];
@@ -25,26 +25,6 @@ class UsersContext {
         $this->modNotes = new ModNotes($dbConn);
     }
 
-    public function getUsers(): Users {
-        return $this->users;
-    }
-
-    public function getRoles(): Roles {
-        return $this->roles;
-    }
-
-    public function getBans(): Bans {
-        return $this->bans;
-    }
-
-    public function getWarnings(): Warnings {
-        return $this->warnings;
-    }
-
-    public function getModNotes(): ModNotes {
-        return $this->modNotes;
-    }
-
     public function getUserInfo(string $value, int|string|null $select = null): UserInfo {
         $select = Users::resolveGetUserSelectAlias($select ?? Users::GET_USER_ID);
         if(($select & Users::GET_USER_ID) > 0 && array_key_exists($value, $this->userInfos))
diff --git a/src/Users/UsersRpcHandler.php b/src/Users/UsersRpcHandler.php
index 40c13197..2bcc6b37 100644
--- a/src/Users/UsersRpcHandler.php
+++ b/src/Users/UsersRpcHandler.php
@@ -39,36 +39,36 @@ final class UsersRpcHandler implements RpcHandler {
             $colourCSS = (string)ColourRgb::convert($colour);
         }
 
-        $baseUrl = $this->siteInfo->getURL();
+        $baseUrl = $this->siteInfo->url;
 
         $output = [];
 
         $output['id'] = $userInfo->getId();
-        $output['name'] = $userInfo->getName();
+        $output['name'] = $userInfo->name;
         if($includeEMailAddress)
-            $output['email'] = $userInfo->getEMailAddress();
+            $output['email'] = $userInfo->emailAddress;
 
         $output['colour_raw'] = $colourRaw;
         $output['colour_css'] = $colourCSS;
         $output['rank'] = $rank;
-        $output['country_code'] = $userInfo->getCountryCode();
+        $output['country_code'] = $userInfo->countryCode;
 
         $roles = XArray::select(
-            $this->usersCtx->getRoles()->getRoles(userInfo: $userInfo, hasString: true, orderByRank: true),
-            fn($roleInfo) => $roleInfo->getString(),
+            $this->usersCtx->roles->getRoles(userInfo: $userInfo, hasString: true, orderByRank: true),
+            fn($roleInfo) => $roleInfo->string,
         );
 
         if(!empty($roles))
             $output['roles'] = $roles;
-        if($userInfo->isSuperUser())
+        if($userInfo->super)
             $output['is_super'] = true;
 
-        if($userInfo->hasTitle())
-            $output['title'] = $userInfo->getTitle();
+        if(!empty($userInfo->title))
+            $output['title'] = $userInfo->title;
 
         $output['created_at'] = $userInfo->getCreatedAt()->toIso8601ZuluString();
-        if($userInfo->hasLastActive())
-            $output['last_active_at'] = $userInfo->getLastActiveAt()->toIso8601ZuluString();
+        if($userInfo->lastActiveTime !== null)
+            $output['last_active_at'] = $userInfo->lastActiveAt->toIso8601ZuluString();
 
         $output['profile_url'] = $baseUrl . $this->urls->format('user-profile', ['user' => $userInfo->getId()]);
         $output['avatar_url'] = $baseUrl . $this->urls->format('user-avatar', ['user' => $userInfo->getId()]);
@@ -88,7 +88,7 @@ final class UsersRpcHandler implements RpcHandler {
         $output['avatar_urls'] = $avatars;
         /* / */
 
-        if($userInfo->isDeleted())
+        if($userInfo->deleted)
             $output['is_deleted'] = true;
 
         return $output;
diff --git a/src/Users/WarningInfo.php b/src/Users/WarningInfo.php
index e249d5f5..b748da81 100644
--- a/src/Users/WarningInfo.php
+++ b/src/Users/WarningInfo.php
@@ -6,11 +6,11 @@ use Index\Db\DbResult;
 
 class WarningInfo {
     public function __construct(
-        private string $id,
-        private string $userId,
-        private ?string $modId,
-        private string $body,
-        private int $created,
+        public private(set) string $id,
+        public private(set) string $userId,
+        public private(set) ?string $modId,
+        public private(set) string $body,
+        public private(set) int $createdTime,
     ) {}
 
     public static function fromResult(DbResult $result): WarningInfo {
@@ -19,43 +19,15 @@ class WarningInfo {
             userId: $result->getString(1),
             modId: $result->getStringOrNull(2),
             body: $result->getString(3),
-            created: $result->getInteger(4)
+            createdTime: $result->getInteger(4)
         );
     }
 
-    public function getId(): string {
-        return $this->id;
+    public array $bodyLines {
+        get => explode("\n", $this->body);
     }
 
-    public function getUserId(): string {
-        return $this->userId;
-    }
-
-    public function hasModId(): bool {
-        return $this->modId !== null;
-    }
-
-    public function getModId(): ?string {
-        return $this->modId;
-    }
-
-    public function hasBody(): bool {
-        return $this->body !== '';
-    }
-
-    public function getBody(): string {
-        return $this->body;
-    }
-
-    public function getBodyLines(): array {
-        return explode("\n", $this->body);
-    }
-
-    public function getCreatedTime(): int {
-        return $this->created;
-    }
-
-    public function getCreatedAt(): CarbonImmutable {
-        return CarbonImmutable::createFromTimestampUTC($this->created);
+    public CarbonImmutable $createdAt {
+        get => CarbonImmutable::createFromTimestampUTC($this->createdTime);
     }
 }
diff --git a/src/Users/Warnings.php b/src/Users/Warnings.php
index 3ae2e5f0..563b1ea1 100644
--- a/src/Users/Warnings.php
+++ b/src/Users/Warnings.php
@@ -154,7 +154,7 @@ class Warnings {
         $args = 0;
         foreach($warnInfos as $warnInfo) {
             if($warnInfo instanceof WarningInfo)
-                $warnInfo = $warnInfo->getId();
+                $warnInfo = $warnInfo->id;
             elseif(!is_string($warnInfo))
                 throw new InvalidArgumentException('$warnInfos must be strings of instances of WarningInfo.');
 
diff --git a/templates/_layout/meta.twig b/templates/_layout/meta.twig
index 8c20066a..f25d28f0 100644
--- a/templates/_layout/meta.twig
+++ b/templates/_layout/meta.twig
@@ -21,7 +21,7 @@
 
     {% if image is defined %}
         {% if image|slice(0, 1) == '/' %}
-            {% set image = globals.site_info.hasURL ? (globals.site_info.url ~ image) : '' %}
+            {% set image = globals.site_info.url is not empty ? (globals.site_info.url ~ image) : '' %}
         {% endif %}
 
         {% if image|length > 0 %}
@@ -31,7 +31,7 @@
 
     {% if canonical_url is defined %}
         {% if canonical_url|slice(0, 1) == '/' %}
-            {% set canonical_url = globals.site_info.hasURL ? (globals.site_info.url ~ canonical_url) : '' %}
+            {% set canonical_url = globals.site_info.url is not empty ? (globals.site_info.url ~ canonical_url) : '' %}
         {% endif %}
 
         {% if canonical_url|length > 0 %}
diff --git a/templates/changelog/change.twig b/templates/changelog/change.twig
index c74c3924..5455b446 100644
--- a/templates/changelog/change.twig
+++ b/templates/changelog/change.twig
@@ -59,7 +59,7 @@
         <div class="changelog__change__text markdown">
             <h1>{{ title }}</h1>
 
-            {% if change_info.hasBody %}
+            {% if change_info.body is not empty %}
                 {{ change_info.body|escape|parse_text(2)|raw }}
             {% else %}
                 <p>This change has no additional notes.</p>
diff --git a/templates/changelog/macros.twig b/templates/changelog/macros.twig
index 28c5fad2..99151987 100644
--- a/templates/changelog/macros.twig
+++ b/templates/changelog/macros.twig
@@ -68,8 +68,8 @@
         </div>
 
         <div class="changelog__entry__text">
-            <a class="changelog__entry__log{% if change.hasBody %} changelog__entry__log--link{% endif %}"
-                {% if change.hasBody %}href="{{ change_url }}"{% endif %}>
+            <a class="changelog__entry__log{% if change.body is not empty %} changelog__entry__log--link{% endif %}"
+                {% if change.body is not empty %}href="{{ change_url }}"{% endif %}>
                 {{ change.summary }}
             </a>
 
diff --git a/templates/forum/macros.twig b/templates/forum/macros.twig
index 33598357..03435c99 100644
--- a/templates/forum/macros.twig
+++ b/templates/forum/macros.twig
@@ -89,7 +89,7 @@
         {% set is_archived = info.forum_archived != 0 %}
     {% else %}
         {% set forum_id = info.id %}
-        {% set is_archived = info.isArchived %}
+        {% set is_archived = info.archived %}
     {% endif %}
 
     {% set is_locked = is_archived %}
@@ -143,7 +143,7 @@
         {% set forum_link_clicks = forum.info.linkClicks %}
         {% set forum_count_topics = forum.info.topicsCount %}
         {% set forum_count_posts = forum.info.postsCount %}
-        {% set forum_show_activity = forum.info.mayHaveTopics or forum.info.hasLinkClicks %}
+        {% set forum_show_activity = forum.info.mayHaveTopics or forum.info.linkClicks is not null %}
         {% set forum_unread = forum.unread %}
         {% set forum_colour = forum.colour %}
 
@@ -340,8 +340,8 @@
     {% set topic_count_posts = topic.info.postsCount %}
     {% set topic_count_views = topic.info.viewsCount %}
     {% set topic_created = topic.info.createdTime %}
-    {% set topic_locked = topic.info.isLocked %}
-    {% set topic_deleted = topic.info.isDeleted %}
+    {% set topic_locked = topic.info.locked %}
+    {% set topic_deleted = topic.info.deleted %}
     {% set topic_pages = (topic.info.postsCount / 10)|round(0, 'ceil') %}
 
     {% set has_topic_author = topic.user is defined %}
@@ -464,7 +464,7 @@
     {% set post_id = post.info.id %}
     {% set post_created = post.info.createdTime %}
     {% set post_edited = post.info.editedTime %}
-    {% set post_is_deleted = post.info.isDeleted %}
+    {% set post_is_deleted = post.info.deleted %}
     {% set post_is_op = post.isOriginalPost %}
     {% set post_body = post.info.body|escape|parse_text(post.info.parser) %}
     {% set post_is_markdown = post.info.isBodyMarkdown %}
@@ -482,7 +482,7 @@
         {% set author_created = post.user.createdTime %}
         {% set author_posts_count = post.postsCount %}
         {% set author_is_op = post.isOriginalPoster %}
-        {% set signature_body = post.user.signatureContent|default('')|escape|parse_text(post.user.signatureParser) %}
+        {% set signature_body = post.user.signatureBody|default('')|escape|parse_text(post.user.signatureBodyParser) %}
         {% set signature_is_markdown = post.user.isSignatureBodyMarkdown %}
     {% endif %}
 
diff --git a/templates/forum/topic.twig b/templates/forum/topic.twig
index a6c73281..4cebe2f2 100644
--- a/templates/forum/topic.twig
+++ b/templates/forum/topic.twig
@@ -19,7 +19,7 @@
 
 {% set forum_post_csrf = csrf_token() %}
 {% set topic_tools = forum_topic_tools(topic_info, topic_pagination, can_reply) %}
-{% set topic_notice = forum_topic_locked(topic_info.lockedTime, category_info.isArchived) ~ forum_topic_redirect(topic_redir_info|default(null)) %}
+{% set topic_notice = forum_topic_locked(topic_info.lockedTime, category_info.archived) ~ forum_topic_redirect(topic_redir_info|default(null)) %}
 {% set topic_actions = [
     {
         'html': '<i class="far fa-trash-alt fa-fw"></i> Delete',
@@ -44,12 +44,12 @@
     {
         'html': '<i class="fas fa-lock fa-fw"></i> Lock',
         'url': url('forum-topic-lock', { topic: topic_info.id, csrf: csrf_token() }),
-        'display': topic_can_lock and not topic_info.isLocked,
+        'display': topic_can_lock and not topic_info.locked,
     },
     {
         'html': '<i class="fas fa-lock-open fa-fw"></i> Unlock',
         'url': url('forum-topic-unlock', { topic: topic_info.id, csrf: csrf_token() }),
-        'display': topic_can_lock and topic_info.isLocked,
+        'display': topic_can_lock and topic_info.locked,
     },
 ] %}
 
diff --git a/templates/manage/forum/redirs.twig b/templates/manage/forum/redirs.twig
index 626fbe53..53ec97ed 100644
--- a/templates/manage/forum/redirs.twig
+++ b/templates/manage/forum/redirs.twig
@@ -54,7 +54,7 @@
                             <div class="manage-list-setting-key-text">{{ redir.topicId }}</div>
                         </td>
                         <td class="manage-list-setting-key">
-                            <div class="manage-list-setting-key-text">{{ redir.hasUserId ? redir.userId : 'System' }}</div>
+                            <div class="manage-list-setting-key-text">{{ redir.userId|default('System') }}</div>
                         </td>
                         <td class="manage-list-setting-value">
                             <div class="manage-list-setting-value-text">{{ redir.linkTarget }}</div>
diff --git a/templates/manage/news/categories.twig b/templates/manage/news/categories.twig
index b87b7633..807e832c 100644
--- a/templates/manage/news/categories.twig
+++ b/templates/manage/news/categories.twig
@@ -11,7 +11,7 @@
             <p>
                 <a href="{{ url('manage-news-category', {'category': cat.id}) }}" class="input__button">#{{ cat.id }}</a>
                 {{ cat.name }} |
-                {{ cat.isHidden ? 'Unlisted' : 'Public' }} |
+                {{ cat.hidden ? 'Unlisted' : 'Public' }} |
                 {{ cat.createdAt }}
             </p>
         {% endfor %}
diff --git a/templates/manage/news/category.twig b/templates/manage/news/category.twig
index c684aa83..8693092e 100644
--- a/templates/manage/news/category.twig
+++ b/templates/manage/news/category.twig
@@ -20,7 +20,7 @@
 
             <tr>
                 <td>Is Hidden</td>
-                <td>{{ input_checkbox('nc_hidden', '', category_info.isHidden|default(false)) }}</td>
+                <td>{{ input_checkbox('nc_hidden', '', category_info.hidden|default(false)) }}</td>
             </tr>
         </table>
 
diff --git a/templates/manage/news/post.twig b/templates/manage/news/post.twig
index 217bba27..5a109a61 100644
--- a/templates/manage/news/post.twig
+++ b/templates/manage/news/post.twig
@@ -20,7 +20,7 @@
 
             <tr>
                 <td>Is Featured</td>
-                <td>{{ input_checkbox('np_featured', '', post_info.isFeatured|default(false)) }}</td>
+                <td>{{ input_checkbox('np_featured', '', post_info.featured|default(false)) }}</td>
             </tr>
 
             <tr>
diff --git a/templates/manage/news/posts.twig b/templates/manage/news/posts.twig
index e32211b6..c61ab94a 100644
--- a/templates/manage/news/posts.twig
+++ b/templates/manage/news/posts.twig
@@ -12,13 +12,13 @@
                 <a href="{{ url('manage-news-post', {'post': post.id}) }}" class="input__button">#{{ post.id }}</a>
                 <a href="{{ url('manage-news-category', {'category': post.categoryId}) }}" class="input__button">Category #{{ post.categoryId }}</a>
                 {{ post.title }} |
-                {{ post.isFeatured ? 'Featured' : 'Normal' }} |
+                {{ post.featured ? 'Featured' : 'Normal' }} |
                 User #{{ post.userId }} |
-                {% if post.hasCommentsCategoryId %}Comments category #{{ post.commentsCategoryId }}{% else %}No comments category{% endif %} |
+                {% if post.commentsSectionId is not null %}Comments category #{{ post.commentsCategoryId }}{% else %}No comments category{% endif %} |
                 Created {{ post.createdAt }} |
-                {{ post.isPublished ? 'published' : 'Published ' ~ post.scheduledAt }} |
-                {{ post.isEdited ? 'Edited ' ~ post.updatedAt : 'not edited' }} |
-                {{ post.isDeleted ? 'Deleted ' ~ post.deletedAt : 'not deleted' }}
+                {{ post.published ? 'published' : 'Published ' ~ post.scheduledAt }} |
+                {{ post.edited ? 'Edited ' ~ post.updatedAt : 'not edited' }} |
+                {{ post.deleted ? 'Deleted ' ~ post.deletedAt : 'not deleted' }}
             </p>
         {% endfor %}
 
diff --git a/templates/manage/users/bans.twig b/templates/manage/users/bans.twig
index 67c651ee..b69a87ec 100644
--- a/templates/manage/users/bans.twig
+++ b/templates/manage/users/bans.twig
@@ -47,7 +47,7 @@
                                 <time datetime="{{ ban.info.createdTime|date('c') }}" title="{{ ban.info.createdTime|date('r') }}">{{ ban.info.createdTime|time_format }}</time>
                             </div>
                         </div>
-                        {% if ban.info.isPermanent %}
+                        {% if ban.info.permanent %}
                             <div class="manage__bans__item__attribute manage__bans__item__permanent">
                                 <div class="manage__bans__item__permanent__icon"><i class="fas fa-dumpster"></i></div>
                                 <div class="manage__bans__item__permanent__time">
@@ -58,13 +58,13 @@
                             <div class="manage__bans__item__attribute manage__bans__item__expires">
                                 <div class="manage__bans__item__expires__icon"><i class="fas fa-stopwatch"></i></div>
                                 <div class="manage__bans__item__expires__time" title="{{ ban.info.expiresTime|date('r') }}">
-                                    {% if ban.info.isActive %}
+                                    {% if ban.info.active %}
                                         <span>{{ ban.info.remainingString }} remaining</span>
                                     {% else %}
                                         <span>{{ ban.info.durationString }}</span>
                                     {% endif %}
                                 </div>
-                                {% if ban.info.isActive %}
+                                {% if ban.info.active %}
                                     <div class="manage__bans__item__expires__status manage__bans__item__expires__status--active">
                                         <span>active</span>
                                     </div>
@@ -94,7 +94,7 @@
                         <a href="{{ url('manage-users-ban-delete', { ban: ban.info.id, csrf: csrf_token() }) }}" title="Revoke/Delete" class="input__button input__button--autosize input__button--destroy manage__bans__item__action" onclick="return confirm('Are you sure?');"><i class="fas fa-times fa-fw"></i></a>
                     </div>
                 </div>
-                {% if ban.info.hasPublicReason %}
+                {% if ban.info.publicReason is not empty %}
                     <div class="manage__bans__item__reason">
                         <div class="manage__bans__item__reason__title">Reason displayed publicly and to the user themselves:</div>
                         <div class="manage__bans__item__reason__body">{{ ban.info.publicReason }}</div>
@@ -104,7 +104,7 @@
                         <div class="manage__bans__item__reason__title">This ban does not display any reason.</div>
                     </div>
                 {% endif %}
-                {% if ban.info.hasPrivateReason %}
+                {% if ban.info.privateReason is not empty %}
                     <div class="manage__bans__item__reason">
                         <div class="manage__bans__item__reason__title">Additional information for moderators:</div>
                         <div class="manage__bans__item__reason__body">{{ ban.info.privateReason }}</div>
diff --git a/templates/manage/users/note.twig b/templates/manage/users/note.twig
index c039e096..9160d8e3 100644
--- a/templates/manage/users/note.twig
+++ b/templates/manage/users/note.twig
@@ -55,7 +55,7 @@
                 </div>
             </div>
 
-            {% if not note_new and note_info.hasBody %}
+            {% if not note_new and note_info.body is not empty %}
                 <div class="manage__note__body markdown manage__note--viewing">
                     {{ note_info.body|escape|parse_text(2)|raw }}
                 </div>
diff --git a/templates/manage/users/notes.twig b/templates/manage/users/notes.twig
index e0aebf20..c0f8d510 100644
--- a/templates/manage/users/notes.twig
+++ b/templates/manage/users/notes.twig
@@ -68,7 +68,7 @@
                         {% endif %}
                     </div>
                 </div>
-                {% if note.info.hasBody %}
+                {% if note.info.body is not empty %}
                     <div class="manage__notes__item__body markdown">
                         {% if notes_filtering %}
                             {{ note.info.body|escape|parse_text(2)|raw }}
diff --git a/templates/manage/users/users.twig b/templates/manage/users/users.twig
index 8e196cb1..db3fac29 100644
--- a/templates/manage/users/users.twig
+++ b/templates/manage/users/users.twig
@@ -15,7 +15,7 @@
 
         <div class="manage__users__collection">
             {% for user in manage_users %}
-                <div class="manage__user-item{% if user.info.isDeleted %} manage__user-item--deleted{% endif %}" style="--accent-colour: {{ user.colour }}">
+                <div class="manage__user-item{% if user.info.deleted %} manage__user-item--deleted{% endif %}" style="--accent-colour: {{ user.colour }}">
                     <a href="{{ url('manage-user', {'user': user.info.id}) }}" class="manage__user-item__background"></a>
 
                     <div class="manage__user-item__container">
@@ -30,14 +30,14 @@
                                     <time datetime="{{ user.info.createdTime|date('c') }}" title="{{ user.info.createdTime|date('r') }}">{{ user.info.createdTime|time_format }}</time> /
                                     <span>{{ user.info.registerRemoteAddress }}</span>
                                 </div>
-                                {% if user.info.hasLastActive %}
+                                {% if user.info.lastActiveTime is not null %}
                                     <div class="manage__user-item__detail">
                                         <i class="fas fa-user-clock fa-fw"></i>
                                         <time datetime="{{ user.info.lastActiveTime|date('c') }}" title="{{ user.info.lastActiveTime|date('r') }}">{{ user.info.lastActiveTime|time_format }}</time> /
                                         <span>{{ user.info.lastRemoteAddress }}</span>
                                     </div>
                                 {% endif %}
-                                {% if user.info.isDeleted %}
+                                {% if user.info.deleted %}
                                     <div class="manage__user-item__detail">
                                         <i class="fas fa-trash-alt fa-fw"></i>
                                         <time datetime="{{ user.info.deletedTime|date('c') }}" title="{{ user.info.deletedTime|date('r') }}">{{ user.info.deletedTime|time_format }}</time>
diff --git a/templates/master.twig b/templates/master.twig
index 51b3b1b9..72ffa4be 100644
--- a/templates/master.twig
+++ b/templates/master.twig
@@ -35,8 +35,8 @@
 {% if globals.active_ban_info is not null %}
             <div class="warning warning--red">
                 <div class="warning__content">
-                    <p>You have been banned {% if globals.active_ban_info.isPermanent %}<strong>permanently</strong>{% else %}for <strong title="{{ globals.active_ban_info.expiresTime|date('r') }}">{{ globals.active_ban_info.remainingString }}</strong>{% endif %} since <strong><time datetime="{{ globals.active_ban_info.createdTime|date('c') }}" title="{{ globals.active_ban_info.createdTime|date('r') }}">{{ globals.active_ban_info.createdTime|time_format }}</time></strong>.</p>
-                    {% if globals.active_ban_info.hasPublicReason %}
+                    <p>You have been banned {% if globals.active_ban_info.permanent %}<strong>permanently</strong>{% else %}for <strong title="{{ globals.active_ban_info.expiresTime|date('r') }}">{{ globals.active_ban_info.remainingString }}</strong>{% endif %} since <strong><time datetime="{{ globals.active_ban_info.createdTime|date('c') }}" title="{{ globals.active_ban_info.createdTime|date('r') }}">{{ globals.active_ban_info.createdTime|time_format }}</time></strong>.</p>
+                    {% if globals.active_ban_info.publicReason is not empty %}
                         <p>Reason: {{ globals.active_ban_info.publicReason }}</p>
                     {% endif %}
                 </div>
diff --git a/templates/messages/index.twig b/templates/messages/index.twig
index 113f03be..197bc450 100644
--- a/templates/messages/index.twig
+++ b/templates/messages/index.twig
@@ -64,10 +64,10 @@
                         {% for message in folder_messages %}
                             {% set user_info = (folder_name == 'drafts' or folder_name == 'sent' ? message.recipient_info : message.author_info) %}
                             {% set user_colour = (folder_name == 'drafts' or folder_name == 'sent' ? message.recipient_colour : message.author_colour) %}
-                            <div class="messages-folder-item messages-entry js-messages-entry" tabindex="0" data-msg-id="{{ message.info.id }}" data-msg-url="{{ url('messages-view', {message: message.info.id}) }}" data-msg-sent="{{ message.info.isSent ? 'sent' : 'draft' }}" data-msg-read="{{ message.info.isRead ? 'read' : 'unread' }}" style="--user-colour: {{ user_colour }};">
+                            <div class="messages-folder-item messages-entry js-messages-entry" tabindex="0" data-msg-id="{{ message.info.id }}" data-msg-url="{{ url('messages-view', {message: message.info.id}) }}" data-msg-sent="{{ message.info.sent ? 'sent' : 'draft' }}" data-msg-read="{{ message.info.read ? 'read' : 'unread' }}" style="--user-colour: {{ user_colour }};">
                                 <div class="messages-entry-header">
                                     <div class="messages-entry-check"><input type="checkbox" class="js-entry-checkbox"></div>
-                                    <div class="messages-entry-unread js-messages-entry-unread"{% if not message.info.isSent or message.info.isRead %} hidden{% endif %}><div class="messages-entry-unread-orb"></div></div>
+                                    <div class="messages-entry-unread js-messages-entry-unread"{% if not message.info.sent or message.info.read %} hidden{% endif %}><div class="messages-entry-unread-orb"></div></div>
                                     <div class="messages-entry-author"><div class="messages-entry-overflow">{{ user_info.name|default('Deleted User') }}</div></div>
                                     <div class="messages-entry-spacing"></div>
                                     <div class="messages-entry-datetime"><time datetime="{{ message.info.displayTime|date('c') }}" title="{{ message.info.displayTime|date('r') }}">{{ message.info.displayTime|time_format }}</time></div>
diff --git a/templates/messages/thread.twig b/templates/messages/thread.twig
index 4628428e..6443e974 100644
--- a/templates/messages/thread.twig
+++ b/templates/messages/thread.twig
@@ -16,15 +16,15 @@
                 <div class="container messages-sidebar-section messages-actions">
                     {{ container_title('<i class="fas fa-comments fa-fw"></i> Actions') }}
 
-                    <button class="messages-actions-item js-messages-actions-mark-read"{% if not message.info.isSent %} hidden{% endif %} data-state="active" data-inactive-str="Mark as read" data-inactive-ico="fas fa-envelope-open fa-fw" data-active-str="Mark as unread" data-active-ico="fas fa-envelope">
+                    <button class="messages-actions-item js-messages-actions-mark-read"{% if not message.info.sent %} hidden{% endif %} data-state="active" data-inactive-str="Mark as read" data-inactive-ico="fas fa-envelope-open fa-fw" data-active-str="Mark as unread" data-active-ico="fas fa-envelope">
                         <div class="messages-actions-item-icon js-messages-button-icon"><i></i></div>
                         <div class="messages-actions-item-label js-messages-button-label"></div>
                     </button>
-                    <button class="messages-actions-item js-messages-actions-move-trash" data-state="{{ message.info.isDeleted ? 'active' : 'inactive' }}" data-inactive-str="Move to Trash" data-inactive-ico="fas fa-trash-alt fa-fw" data-active-str="Restore item" data-active-ico="fas fa-trash-restore-alt">
+                    <button class="messages-actions-item js-messages-actions-move-trash" data-state="{{ message.info.deleted ? 'active' : 'inactive' }}" data-inactive-str="Move to Trash" data-inactive-ico="fas fa-trash-alt fa-fw" data-active-str="Restore item" data-active-ico="fas fa-trash-restore-alt">
                         <div class="messages-actions-item-icon js-messages-button-icon"><i></i></div>
                         <div class="messages-actions-item-label js-messages-button-label"></div>
                     </button>
-                    <button class="messages-actions-item js-messages-actions-nuke"{% if not message.info.isDeleted %} hidden{% endif %}>
+                    <button class="messages-actions-item js-messages-actions-nuke"{% if not message.info.deleted %} hidden{% endif %}>
                         <div class="messages-actions-item-icon"><i class="fas fa-radiation-alt"></i></div>
                         <div class="messages-actions-item-label">Permanently delete</div>
                     </button>
@@ -34,7 +34,7 @@
         <div class="messages-columns-content">
             <div class="messages-thread js-messages-thread">
                 {% if reply_to is defined and reply_to is not empty %}
-                    <article class="{{ html_classes('container', 'messages-message', 'messages-message-snippet', 'js-messages-message', {'messages-message-deleted': reply_to.info.isDeleted, 'messages-message-draft': not reply_to.info.isSent}) }}" tabindex="0" data-msg-id="{{ reply_to.info.id }}" data-msg-url="{{ url('messages-view', {message: reply_to.info.id}) }}" data-msg-type="snip" data-msg-sent="{{ reply_to.info.isSent ? 'sent' : 'draft' }}" data-msg-read="{{ reply_to.info.isRead ? 'read' : 'unread' }}">
+                    <article class="{{ html_classes('container', 'messages-message', 'messages-message-snippet', 'js-messages-message', {'messages-message-deleted': reply_to.info.deleted, 'messages-message-draft': not reply_to.info.sent}) }}" tabindex="0" data-msg-id="{{ reply_to.info.id }}" data-msg-url="{{ url('messages-view', {message: reply_to.info.id}) }}" data-msg-type="snip" data-msg-sent="{{ reply_to.info.sent ? 'sent' : 'draft' }}" data-msg-read="{{ reply_to.info.read ? 'read' : 'unread' }}">
                         <div class="messages-message-header">
                             <div class="messages-message-sender-avatar">
                                 {{ avatar(reply_to.author_info.id|default(0), 80) }}
@@ -66,7 +66,7 @@
                     </article>
                 {% endif %}
 
-                <article class="{{ html_classes('container', 'messages-message', 'js-messages-message', {'messages-message-deleted': message.info.isDeleted, 'messages-message-draft': not message.info.isSent}) }}" data-msg-id="{{ message.info.id }}" data-msg-url="{{ url('messages-view', {message: message.info.id}) }}" data-msg-type="full" data-msg-sent="{{ message.info.isSent ? 'sent' : 'draft' }}" data-msg-read="{{ message.info.isRead ? 'read' : 'unread' }}" data-msg-deleted="{{ message.info.isDeleted ? 'yes' : 'no' }}">
+                <article class="{{ html_classes('container', 'messages-message', 'js-messages-message', {'messages-message-deleted': message.info.deleted, 'messages-message-draft': not message.info.sent}) }}" data-msg-id="{{ message.info.id }}" data-msg-url="{{ url('messages-view', {message: message.info.id}) }}" data-msg-type="full" data-msg-sent="{{ message.info.sent ? 'sent' : 'draft' }}" data-msg-read="{{ message.info.read ? 'read' : 'unread' }}" data-msg-deleted="{{ message.info.deleted ? 'yes' : 'no' }}">
                     <div class="messages-message-header">
                         <div class="messages-message-sender-avatar">
                             {{ avatar(message.author_info.id|default(0), 80) }}
@@ -98,7 +98,7 @@
                 {% if can_send_messages %}
                     {% set has_draft_info = draft_info is defined and draft_info is not null %}
                     {% set reply_field_is_draft = false %}
-                    {% if not has_draft_info and not message.info.isSent %}
+                    {% if not has_draft_info and not message.info.sent %}
                         {% set has_draft_info = true %}
                         {% set reply_field_is_draft = true %}
                         {% set draft_info = message.info %}
@@ -108,7 +108,7 @@
                     {% set msg_recipient_id = message.info.recipientId|default(0) %}
 
                     {% if has_draft_info or (msg_author_id > 0 and msg_recipient_id > 0) %}
-                        <div class="container messages-reply js-messages-reply"{% if (reply_field_is_draft ? draft_info.isDeleted : message.info.isDeleted) %} hidden{% endif %}>
+                        <div class="container messages-reply js-messages-reply"{% if (reply_field_is_draft ? draft_info.deleted : message.info.deleted) %} hidden{% endif %}>
                             {{ container_title(has_draft_info ? '<i class="fas fa-edit"></i> Edit' : '<i class="fas fa-reply"></i> Reply') }}
 
                             <form class="messages-reply-form js-messages-reply-form">
@@ -141,7 +141,7 @@
 
                 {% if replies_for is defined and replies_for is iterable %}
                     {% for reply_for in replies_for %}
-                        <article class="{{ html_classes('container', 'messages-message', 'messages-message-snippet', 'js-messages-message', {'messages-message-deleted': reply_for.info.isDeleted, 'messages-message-draft': not reply_for.info.isSent}) }}" tabindex="0" data-msg-id="{{ reply_for.info.id }}" data-msg-url="{{ url('messages-view', {message: reply_for.info.id}) }}" data-msg-type="snip" data-msg-sent="{{ reply_for.info.isSent ? 'sent' : 'draft' }}" data-msg-read="{{ reply_for.info.isRead ? 'read' : 'unread' }}">
+                        <article class="{{ html_classes('container', 'messages-message', 'messages-message-snippet', 'js-messages-message', {'messages-message-deleted': reply_for.info.deleted, 'messages-message-draft': not reply_for.info.sent}) }}" tabindex="0" data-msg-id="{{ reply_for.info.id }}" data-msg-url="{{ url('messages-view', {message: reply_for.info.id}) }}" data-msg-type="snip" data-msg-sent="{{ reply_for.info.sent ? 'sent' : 'draft' }}" data-msg-read="{{ reply_for.info.read ? 'read' : 'unread' }}">
                             <div class="messages-message-header">
                                 <div class="messages-message-sender-avatar">
                                     {{ avatar(reply_for.author_info.id|default(0), 80) }}
diff --git a/templates/news/macros.twig b/templates/news/macros.twig
index 3ad44c7d..eca74d4a 100644
--- a/templates/news/macros.twig
+++ b/templates/news/macros.twig
@@ -78,7 +78,7 @@
                     </time>
                 </div>
 
-                {% if post.isEdited %}
+                {% if post.edited %}
                     <div class="news__post__date">
                         Updated
                         <time datetime="{{ post.updatedTime|date('c') }}" title="{{ post.updatedTime|date('r') }}">
diff --git a/templates/profile/_layout/header.twig b/templates/profile/_layout/header.twig
index 9ba47306..406bdacd 100644
--- a/templates/profile/_layout/header.twig
+++ b/templates/profile/_layout/header.twig
@@ -35,13 +35,13 @@
                     {{ profile_user.name }}
                 </div>
 
-                {% if profile_user.hasTitle %}
+                {% if profile_user.title is not empty %}
                     <div class="profile__header__title">
                         {{ profile_user.title }}
                     </div>
                 {% endif %}
 
-                {% set hasCountryCode = profile_user.hasCountryCode %}
+                {% set hasCountryCode = profile_user.countryCode != 'XX' %}
                 {% set age = profile_user.age %}
                 {% set hasAge = age > 0 %}
                 {% if hasCountryCode or hasAge %}
@@ -122,8 +122,8 @@
 {% if profile_is_banned %}
     <div class="warning warning--red warning--bigger">
         <div class="warning__content">
-            This user has been banned {% if profile_ban_info.isPermanent %}<strong>permanently</strong>{% else %}for <strong title="{{ profile_ban_info.expiresTime|date('r') }}">{{ profile_ban_info.remainingString }}</strong>{% endif %} since <strong><time datetime="{{ profile_ban_info.createdTime|date('c') }}" title="{{ profile_ban_info.createdTime|date('r') }}">{{ profile_ban_info.createdTime|time_format }}</time></strong>.
-            {% if not profile_is_guest and profile_ban_info.hasPublicReason %}
+            This user has been banned {% if profile_ban_info.permanent %}<strong>permanently</strong>{% else %}for <strong title="{{ profile_ban_info.expiresTime|date('r') }}">{{ profile_ban_info.remainingString }}</strong>{% endif %} since <strong><time datetime="{{ profile_ban_info.createdTime|date('c') }}" title="{{ profile_ban_info.createdTime|date('r') }}">{{ profile_ban_info.createdTime|time_format }}</time></strong>.
+            {% if not profile_is_guest and profile_ban_info.publicReason is not empty %}
                 <p>Reason: {{ profile_ban_info.publicReason }}</p>
             {% endif %}
         </div>
diff --git a/templates/profile/index.twig b/templates/profile/index.twig
index 5f0df69f..eda19eaa 100644
--- a/templates/profile/index.twig
+++ b/templates/profile/index.twig
@@ -176,7 +176,7 @@
                                             Most active topic
                                         </div>
 
-                                        <div class="forum__topic{% if profile_active_topic_info.isLocked %} forum__topic--locked{% endif %}">
+                                        <div class="forum__topic{% if profile_active_topic_info.locked %} forum__topic--locked{% endif %}">
                                             <a href="{{ url('forum-topic', {'topic': profile_active_topic_info.id}) }}" class="forum__topic__link"></a>
 
                                             <div class="forum__topic__container">
@@ -260,35 +260,35 @@
 
                 {% if profile_user is defined %}
                     <div class="profile__content__main">
-                        {% if (not profile_is_banned or profile_can_edit) and ((profile_is_editing and perms.edit_about) or profile_user.hasAboutContent) %}
+                        {% if (not profile_is_banned or profile_can_edit) and ((profile_is_editing and perms.edit_about) or profile_user.aboutBody is not empty) %}
                             <div class="container profile__container profile__about" id="about">
                                 {{ container_title('About ' ~ profile_user.name) }}
 
                                 {% if profile_is_editing %}
                                     <div class="profile__signature__editor">
-                                        {{ input_select('about[parser]', constant('\\Misuzu\\Parsers\\Parser::NAMES'), profile_user.aboutParser, '', '', false, 'profile__about__select') }}
-                                        <textarea name="about[text]" class="input__textarea profile__about__text" id="about-textarea">{{ profile_user.aboutContent }}</textarea>
+                                        {{ input_select('about[parser]', constant('\\Misuzu\\Parsers\\Parser::NAMES'), profile_user.aboutBodyParser, '', '', false, 'profile__about__select') }}
+                                        <textarea name="about[text]" class="input__textarea profile__about__text" id="about-textarea">{{ profile_user.aboutBody }}</textarea>
                                     </div>
                                 {% else %}
-                                    <div class="profile__about__content{% if profile_is_editing %} profile__about__content--edit{% elseif profile_user.aboutParser == constant('\\Misuzu\\Parsers\\Parser::MARKDOWN') %} markdown{% endif %}">
-                                        {{ profile_user.aboutContent|escape|parse_text(profile_user.aboutParser)|raw }}
+                                    <div class="profile__about__content{% if profile_is_editing %} profile__about__content--edit{% elseif profile_user.isAboutBodyMarkdown %} markdown{% endif %}">
+                                        {{ profile_user.aboutBody|escape|parse_text(profile_user.aboutBodyParser)|raw }}
                                     </div>
                                 {% endif %}
                             </div>
                         {% endif %}
 
-                        {% if (not profile_is_banned or profile_can_edit) and ((profile_is_editing and perms.edit_signature) or profile_user.hasSignatureContent) %}
+                        {% if (not profile_is_banned or profile_can_edit) and ((profile_is_editing and perms.edit_signature) or profile_user.signatureBody is not empty) %}
                             <div class="container profile__container profile__signature" id="signature">
                                 {{ container_title('Signature') }}
 
                                 {% if profile_is_editing %}
                                     <div class="profile__signature__editor">
-                                        {{ input_select('signature[parser]', constant('\\Misuzu\\Parsers\\Parser::NAMES'), profile_user.signatureParser, '', '', false, 'profile__signature__select') }}
-                                        <textarea name="signature[text]" class="input__textarea profile__signature__text" id="signature-textarea">{{ profile_user.signatureContent }}</textarea>
+                                        {{ input_select('signature[parser]', constant('\\Misuzu\\Parsers\\Parser::NAMES'), profile_user.signatureBodyParser, '', '', false, 'profile__signature__select') }}
+                                        <textarea name="signature[text]" class="input__textarea profile__signature__text" id="signature-textarea">{{ profile_user.signatureBody }}</textarea>
                                     </div>
                                 {% else %}
-                                    <div class="profile__signature__content{% if profile_is_editing %} profile__signature__content--edit{% elseif profile_user.signatureParser == constant('\\Misuzu\\Parsers\\Parser::MARKDOWN') %} markdown{% endif %}">
-                                        {{ profile_user.signatureContent|escape|parse_text(profile_user.signatureParser)|raw }}
+                                    <div class="profile__signature__content{% if profile_is_editing %} profile__signature__content--edit{% elseif profile_user.isSignatureBodyMarkdown %} markdown{% endif %}">
+                                        {{ profile_user.signatureBody|escape|parse_text(profile_user.signatureBodyParser)|raw }}
                                     </div>
                                 {% endif %}
                             </div>
diff --git a/templates/settings/account.twig b/templates/settings/account.twig
index 1800a36c..3246b9ca 100644
--- a/templates/settings/account.twig
+++ b/templates/settings/account.twig
@@ -128,7 +128,7 @@
             {% endif %}
 
             <div class="settings__two-factor__settings">
-                {% if settings_user.hasTOTPKey %}
+                {% if settings_user.hasTOTP %}
                     <div class="settings__two-factor__settings__status">
                         <i class="fas fa-lock fa-fw"></i> Two Factor Authentication is enabled!
                     </div>
diff --git a/templates/user/macros.twig b/templates/user/macros.twig
index e238dc29..032a84da 100644
--- a/templates/user/macros.twig
+++ b/templates/user/macros.twig
@@ -10,11 +10,8 @@
         {% set info = {
             'id': user.user_id,
             'name': user.username,
-            'hasTitle': user.user_title is defined and user.user_title is not empty,
             'title': user.user_title|default(''),
-            'hasCountryCode': user.user_country is defined and user.user_country != 'XX',
             'countryCode': user.user_country|default('XX'),
-            'hasLastActive': user.user_active is defined and user.user_active > 0,
             'lastActiveTime': user.user_active,
             'createdTime': user.user_created
         } %}
@@ -36,13 +33,13 @@
                     {{ info.name }}
                 </div>
 
-                {% if info.hasTitle %}
+                {% if info.title is not empty %}
                     <div class="usercard__details__title">
                         {{ info.title }}
                     </div>
                 {% endif %}
 
-                {% if info.hasCountryCode %}
+                {% if info.countryCode != 'XX' %}
                     <div class="usercard__details__country">
                         <div class="flag flag--{{ info.countryCode|lower }}"></div>
                         <div class="usercard__details__country__name">
@@ -68,7 +65,7 @@
                 </a>
             {% endif %}
 
-            {% if info.hasLastActive %}
+            {% if info.lastActiveTime is not null %}
                 <div class="usercard__stat usercard__stat--wide" title="{{ info.lastActiveTime|date('r') }}">
                     <div class="usercard__stat__name">Last seen</div>
                     <div class="usercard__stat__value"><time datetime="{{ info.lastActiveTime|date('c') }}">{{ info.lastActiveTime|time_format }}</time></div>
@@ -119,7 +116,7 @@
                     </div>
                 </div>
 
-                {% if session.hasLastRemoteAddress %}
+                {% if session.lastRemoteAddress is not null %}
                     <div class="settings__session__detail">
                         <div class="settings__session__detail__title">
                             Last used from IP
@@ -148,7 +145,7 @@
                     </time>
                 </div>
 
-                {% if session.hasLastActive %}
+                {% if session.lastActiveTime is not null %}
                     <div class="settings__session__detail" title="{{ session.lastActiveTime|date('r') }}">
                         <div class="settings__session__detail__title">
                             Last Active
@@ -228,7 +225,7 @@
     {% from 'macros.twig' import avatar %}
 
     <div class="settings__account-log">
-        {% if data.hasUserId and users[data.userId] is defined %}
+        {% if data.userId is not null and users[data.userId] is defined %}
             {% set user = users[data.userId] %}
             {% set colour = colours[data.userId]|default(null) %}
             <a href="{{ url('user-profile', {'user': user.id}) }}" class="settings__account-log__user"{% if colour is not null %} style="--user-colour: {{ colour }}"{% endif %}>
diff --git a/tools/cron b/tools/cron
index 096954dc..5919cdb1 100755
--- a/tools/cron
+++ b/tools/cron
@@ -75,7 +75,7 @@ msz_sched_task_sql('Synchronise forum_id.', true,
     'UPDATE msz_forum_posts AS p INNER JOIN msz_forum_topics AS t ON t.topic_id = p.topic_id SET p.forum_id = t.forum_id');
 
 msz_sched_task_func('Recount forum topics and posts.', true, function() use ($msz) {
-    $msz->getForumContext()->getCategories()->syncForumCounters();
+    $msz->forumCtx->categories->syncForumCounters();
 });
 
 msz_sched_task_sql('Clean up expired 2fa tokens.', false,
@@ -84,9 +84,6 @@ msz_sched_task_sql('Clean up expired 2fa tokens.', false,
 // very heavy stuff that should
 
 msz_sched_task_func('Resync statistics counters.', true, function() use ($msz) {
-    $dbConn = $msz->getDbConn();
-    $counters = $msz->getCounters();
-
     $stats = [
         'users:total'                  => 'SELECT COUNT(*) FROM msz_users',
         'users:active'                 => 'SELECT COUNT(*) FROM msz_users WHERE user_active IS NOT NULL AND user_deleted IS NULL',
@@ -144,27 +141,26 @@ msz_sched_task_func('Resync statistics counters.', true, function() use ($msz) {
     ];
 
     foreach($stats as $name => $query) {
-        $result = $dbConn->query($query);
-        $counters->set($name, $result->next() ? $result->getInteger(0) : 0);
+        $result = $msz->dbConn->query($query);
+        $msz->counters->set($name, $result->next() ? $result->getInteger(0) : 0);
     }
 });
 
 msz_sched_task_func('Recalculate permissions (maybe)...', false, function() use ($msz) {
-    $needsRecalc = $msz->getConfig()->getBoolean('perms.needsRecalc');
+    $needsRecalc = $msz->config->getBoolean('perms.needsRecalc');
     if(!$needsRecalc)
         return;
 
-    $msz->getConfig()->removeValues('perms.needsRecalc');
-    $msz->getPerms()->precalculatePermissions($msz->getForumContext()->getCategories());
+    $msz->config->removeValues('perms.needsRecalc');
+    $msz->perms->precalculatePermissions($msz->forumCtx->categories);
 });
 
 msz_sched_task_func('Omega disliking comments...', true, function() use ($msz) {
-    $commentIds = $msz->getConfig()->getArray('comments.omegadislike');
+    $commentIds = $msz->config->getArray('comments.omegadislike');
     if(!is_array($commentIds))
         return;
 
-    $dbConn = $msz->getDbConn();
-    $stmt = $dbConn->prepare('REPLACE INTO msz_comments_votes (comment_id, user_id, comment_vote) SELECT ?, user_id, -1 FROM msz_users');
+    $stmt = $msz->dbConn->prepare('REPLACE INTO msz_comments_votes (comment_id, user_id, comment_vote) SELECT ?, user_id, -1 FROM msz_users');
     foreach($commentIds as $commentId) {
         $stmt->addParameter(1, $commentId);
         $stmt->execute();
@@ -172,14 +168,13 @@ msz_sched_task_func('Omega disliking comments...', true, function() use ($msz) {
 });
 
 msz_sched_task_func('Announcing random topics...', true, function() use ($msz) {
-    $categoryIds = $msz->getConfig()->getArray('forum.rngannounce');
+    $categoryIds = $msz->config->getArray('forum.rngannounce');
     if(!is_array($categoryIds))
         return;
 
-    $dbConn = $msz->getDbConn();
-    $stmtRevert = $dbConn->prepare('UPDATE msz_forum_topics SET topic_type = 0 WHERE forum_id = ? AND topic_type = 2');
-    $stmtRandom = $dbConn->prepare('SELECT topic_id FROM msz_forum_topics WHERE forum_id = ? AND topic_deleted IS NULL ORDER BY RAND() LIMIT 1');
-    $stmtAnnounce = $dbConn->prepare('UPDATE msz_forum_topics SET topic_type = 2 WHERE topic_id = ?');
+    $stmtRevert = $msz->dbConn->prepare('UPDATE msz_forum_topics SET topic_type = 0 WHERE forum_id = ? AND topic_type = 2');
+    $stmtRandom = $msz->dbConn->prepare('SELECT topic_id FROM msz_forum_topics WHERE forum_id = ? AND topic_deleted IS NULL ORDER BY RAND() LIMIT 1');
+    $stmtAnnounce = $msz->dbConn->prepare('UPDATE msz_forum_topics SET topic_type = 2 WHERE topic_id = ?');
     foreach($categoryIds as $categoryId) {
         $stmtRevert->addParameter(1, $categoryId);
         $stmtRevert->execute();
@@ -196,18 +191,15 @@ msz_sched_task_func('Announcing random topics...', true, function() use ($msz) {
 });
 
 msz_sched_task_func('Changing category icons...', false, function() use ($msz) {
-    $config = $msz->getConfig();
-    $categoryIds = $config->getArray('forum.rngicon');
+    $categoryIds = $msz->config->getArray('forum.rngicon');
     if(!is_array($categoryIds))
         return;
 
-    $dbConn = $msz->getDbConn();
-
-    $stmtIcon = $dbConn->prepare('UPDATE msz_forum_categories SET forum_icon = COALESCE(?, forum_icon), forum_name = COALESCE(?, forum_name), forum_colour = ((ROUND(RAND() * 255) << 16) | (ROUND(RAND() * 255) << 8) | ROUND(RAND() * 255)) WHERE forum_id = ?');
-    $stmtUnlock = $dbConn->prepare('UPDATE msz_forum_topics SET topic_locked = IF(?, NULL, COALESCE(topic_locked, NOW())) WHERE topic_id = ?');
+    $stmtIcon = $msz->dbConn->prepare('UPDATE msz_forum_categories SET forum_icon = COALESCE(?, forum_icon), forum_name = COALESCE(?, forum_name), forum_colour = ((ROUND(RAND() * 255) << 16) | (ROUND(RAND() * 255) << 8) | ROUND(RAND() * 255)) WHERE forum_id = ?');
+    $stmtUnlock = $msz->dbConn->prepare('UPDATE msz_forum_topics SET topic_locked = IF(?, NULL, COALESCE(topic_locked, NOW())) WHERE topic_id = ?');
 
     foreach($categoryIds as $categoryId) {
-        $scoped = $config->scopeTo(sprintf('forum.rngicon.fc%d', $categoryId));
+        $scoped = $msz->config->scopeTo(sprintf('forum.rngicon.fc%d', $categoryId));
 
         $icons = $scoped->getArray('icons');
         $icon = $icons[array_rand($icons)];
@@ -242,8 +234,6 @@ msz_sched_task_func('Changing category icons...', false, function() use ($msz) {
 
 echo 'Running ' . count($schedTasks) . ' tasks...' . PHP_EOL;
 
-$dbConn = $msz->getDbConn();
-
 foreach($schedTasks as $task) {
     echo '=> ' . $task->name . PHP_EOL;
     $success = true;
@@ -251,7 +241,7 @@ foreach($schedTasks as $task) {
     try {
         switch($task->type) {
             case 'sql':
-                $affected = $dbConn->execute($task->command);
+                $affected = $msz->dbConn->execute($task->command);
                 echo $affected . ' rows affected. ' . PHP_EOL;
                 break;
 
diff --git a/tools/devel-insert-bogus b/tools/devel-insert-bogus
index 61772a40..369627fe 100755
--- a/tools/devel-insert-bogus
+++ b/tools/devel-insert-bogus
@@ -21,24 +21,21 @@ function mkv_log(string $string): void {
 
 mkv_log('Filling database with markov data...');
 
-mkv_log('Yoinked Database instance from Misuzu...');
-$dbConn = $msz->getDbConn();
-
 mkv_log('Opening role dictionaries...');
 $roleNames = new MarkovDictionary(MKV_DICTS . '/roles_names.fmk');
 $roleDescs = new MarkovDictionary(MKV_DICTS . '/roles_descs.fmk');
 $roleTitles = new MarkovDictionary(MKV_DICTS . '/roles_titles.fmk');
 
 mkv_log('Nuking roles table...');
-$dbConn->execute('DELETE FROM msz_roles WHERE role_id > 1');
-$dbConn->execute('ALTER TABLE msz_roles AUTO_INCREMENT = 2');
+$msz->dbConn->execute('DELETE FROM msz_roles WHERE role_id > 1');
+$msz->dbConn->execute('ALTER TABLE msz_roles AUTO_INCREMENT = 2');
 
 mkv_log('Running slow cron to ensure main role exists...');
 echo shell_exec(MSZ_ROOT . '/tools/cron slow');
 
 mkv_log('Preparing role and permissions insert statements...');
-$cr = $dbConn->prepare('INSERT INTO msz_roles (role_hierarchy, role_name, role_title, role_description, role_hidden, role_can_leave, role_colour) VALUES (?, ?, ?, ?, ?, ?, ?)');
-$cp = $dbConn->prepare('REPLACE INTO msz_permissions (role_id, general_perms_allow, user_perms_allow, changelog_perms_allow, news_perms_allow, forum_perms_allow, comments_perms_allow) VALUES (?, ?, ?, ?, ?, ?, ?)');
+$cr = $msz->dbConn->prepare('INSERT INTO msz_roles (role_hierarchy, role_name, role_title, role_description, role_hidden, role_can_leave, role_colour) VALUES (?, ?, ?, ?, ?, ?, ?)');
+$cp = $msz->dbConn->prepare('REPLACE INTO msz_permissions (role_id, general_perms_allow, user_perms_allow, changelog_perms_allow, news_perms_allow, forum_perms_allow, comments_perms_allow) VALUES (?, ?, ?, ?, ?, ?, ?)');
 
 mkv_log('Adding permissions for main role...');
 $cp->reset();
@@ -62,7 +59,7 @@ $cr->addParameter(6, 0);
 $cr->addParameter(7, 1693465);
 $cr->execute();
 
-$rIdMod = (int)$dbConn->getLastInsertId();
+$rIdMod = (int)$msz->dbConn->getLastInsertId();
 
 mkv_log('Adding permissions for Global Moderator...');
 $cp->reset();
@@ -86,7 +83,7 @@ $cr->addParameter(6, 0);
 $cr->addParameter(7, 16711680);
 $cr->execute();
 
-$rIdAdm = (int)$dbConn->getLastInsertId();
+$rIdAdm = (int)$msz->dbConn->getLastInsertId();
 
 mkv_log('Adding permissions for Administrator...');
 $cp->reset();
@@ -110,7 +107,7 @@ $cr->addParameter(6, 0);
 $cr->addParameter(7, 10390951);
 $cr->execute();
 
-$rIdBot = (int)$dbConn->getLastInsertId();
+$rIdBot = (int)$msz->dbConn->getLastInsertId();
 
 mkv_log('Creating Tester role...');
 $cr->reset();
@@ -123,7 +120,7 @@ $cr->addParameter(6, 1);
 $cr->addParameter(7, 1073741824);
 $cr->execute();
 
-$rIdTest = (int)$dbConn->getLastInsertId();
+$rIdTest = (int)$msz->dbConn->getLastInsertId();
 
 mkv_log('Adding permissions for Tester...');
 $cp->reset();
@@ -158,7 +155,7 @@ $cr->addParameter(6, 0);
 $cr->addParameter(7, 7558084);
 $cr->execute();
 
-$rIdDev = (int)$dbConn->getLastInsertId();
+$rIdDev = (int)$msz->dbConn->getLastInsertId();
 
 mkv_log('Adding permissions for Developer...');
 $cp->reset();
@@ -182,7 +179,7 @@ $cr->addParameter(6, 0);
 $cr->addParameter(7, 15635456);
 $cr->execute();
 
-$rIdTen = (int)$dbConn->getLastInsertId();
+$rIdTen = (int)$msz->dbConn->getLastInsertId();
 
 mkv_log('Adding permissions for Tenshi...');
 $cp->reset();
@@ -215,12 +212,12 @@ $userSigs = new MarkovDictionary(MKV_DICTS . '/users_sigs.fmk');
 $userAbouts = new MarkovDictionary(MKV_DICTS . '/users_abouts.fmk');
 
 mkv_log('Nuking users table...');
-$dbConn->execute('DELETE FROM msz_users');
-$dbConn->execute('ALTER TABLE msz_users AUTO_INCREMENT = 1');
+$msz->dbConn->execute('DELETE FROM msz_users');
+$msz->dbConn->execute('ALTER TABLE msz_users AUTO_INCREMENT = 1');
 
 mkv_log('Preparing user insert statements...');
-$cu = $dbConn->prepare('INSERT INTO msz_users (username, password, email, register_ip, last_ip, user_super, user_country, user_about_content, user_about_parser, user_signature_content, user_signature_parser, user_birthdate, user_title, display_role) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)');
-$ur = $dbConn->prepare('REPLACE INTO msz_users_roles (user_id, role_id) VALUES (?, ?)');
+$cu = $msz->dbConn->prepare('INSERT INTO msz_users (username, password, email, register_ip, last_ip, user_super, user_country, user_about_content, user_about_parser, user_signature_content, user_signature_parser, user_birthdate, user_title, display_role) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)');
+$ur = $msz->dbConn->prepare('REPLACE INTO msz_users_roles (user_id, role_id) VALUES (?, ?)');
 
 mkv_log('Creating admin user...');
 mkv_log('NOTICE: All passwords will be set to: ' . MKV_PASSWD);
@@ -279,7 +276,7 @@ for($i = 1; $i < 2000; ++$i) {
     $cu->addParameter(14, mt_rand(9, 18)); // display role
     $cu->execute();
 
-    $uId = $dbConn->getLastInsertId();
+    $uId = $msz->dbConn->getLastInsertId();
 
     for($j = 0; $j < mt_rand(1, 4); ++$j) {
         $brid = mt_rand(9, 18);
@@ -296,11 +293,11 @@ $changeTagsNames = new MarkovDictionary(MKV_DICTS . '/changes_tags_names.fmk');
 $changeTagsDescs = new MarkovDictionary(MKV_DICTS . '/changes_tags_descs.fmk');
 
 mkv_log('Nuking changelog tags table...');
-$dbConn->execute('DELETE FROM msz_changelog_tags');
-$dbConn->execute('ALTER TABLE msz_changelog_tags AUTO_INCREMENT = 1');
+$msz->dbConn->execute('DELETE FROM msz_changelog_tags');
+$msz->dbConn->execute('ALTER TABLE msz_changelog_tags AUTO_INCREMENT = 1');
 
 mkv_log('Preparing changelog insert statements...');
-$ct = $dbConn->prepare('INSERT INTO msz_changelog_tags (tag_name, tag_description) VALUES (?, ?)');
+$ct = $msz->dbConn->prepare('INSERT INTO msz_changelog_tags (tag_name, tag_description) VALUES (?, ?)');
 $cTagIds = [];
 
 for($i = 0; $i < 20; ++$i) {
@@ -309,7 +306,7 @@ for($i = 0; $i < 20; ++$i) {
     $ct->addParameter(1, mb_substr($changeTagsNames->generate(), 0, 200, 'utf-8') . $i);
     $ct->addParameter(2, mb_substr($changeTagsDescs->generate(), 0, 60000, 'utf-8'));
     $ct->execute();
-    $cTagIds[] = $dbConn->getLastInsertId();
+    $cTagIds[] = $msz->dbConn->getLastInsertId();
 }
 
 mkv_log('Opening changelog changes markov dictionaries...');
@@ -317,12 +314,12 @@ $changeLogs = new MarkovDictionary(MKV_DICTS . '/changes_logs.fmk');
 $changeTexts = new MarkovDictionary(MKV_DICTS . '/changes_texts.fmk');
 
 mkv_log('Nuking changelog changes tables...');
-$dbConn->execute('DELETE FROM msz_changelog_changes');
-$dbConn->execute('ALTER TABLE msz_changelog_changes AUTO_INCREMENT = 1');
+$msz->dbConn->execute('DELETE FROM msz_changelog_changes');
+$msz->dbConn->execute('ALTER TABLE msz_changelog_changes AUTO_INCREMENT = 1');
 
 mkv_log('Preparing changelog changes statements...');
-$cc = $dbConn->prepare('INSERT INTO msz_changelog_changes (user_id, change_action, change_created, change_log, change_text) VALUES (?, ?, FROM_UNIXTIME(?), ?, ?)');
-$ctt = $dbConn->prepare('REPLACE INTO msz_changelog_change_tags (change_id, tag_id) VALUES (?, ?)');
+$cc = $msz->dbConn->prepare('INSERT INTO msz_changelog_changes (user_id, change_action, change_created, change_log, change_text) VALUES (?, ?, FROM_UNIXTIME(?), ?, ?)');
+$ctt = $msz->dbConn->prepare('REPLACE INTO msz_changelog_change_tags (change_id, tag_id) VALUES (?, ?)');
 
 $max = mt_rand(1000, 10000);
 mkv_log('Inserting ' . $max . ' changelog entries...');
@@ -339,7 +336,7 @@ for($i = 0; $i < $max; ++$i) {
     $cc->addParameter(5, mt_rand(0, 100) > 90 ? mb_substr($changeTexts->generate(), 0, 60000, 'utf-8') : null);
     $cc->execute();
 
-    $changeId = $dbConn->getLastInsertId();
+    $changeId = $msz->dbConn->getLastInsertId();
 
     for($j = 0; $j < mt_rand(1, 5); ++$j) {
         $btag = $cTagIds[array_rand($cTagIds)];
@@ -356,11 +353,11 @@ $newsCatsNames = new MarkovDictionary(MKV_DICTS . '/news_cats_names.fmk');
 $newsCatsDescs = new MarkovDictionary(MKV_DICTS . '/news_cats_descs.fmk');
 
 mkv_log('Nuking news categories table...');
-$dbConn->execute('DELETE FROM msz_news_categories');
-$dbConn->execute('ALTER TABLE msz_news_categories AUTO_INCREMENT = 1');
+$msz->dbConn->execute('DELETE FROM msz_news_categories');
+$msz->dbConn->execute('ALTER TABLE msz_news_categories AUTO_INCREMENT = 1');
 
 mkv_log('Preparing news categories insert statements...');
-$nc = $dbConn->prepare('INSERT INTO msz_news_categories (category_name, category_description, category_is_hidden) VALUES (?, ?, ?)');
+$nc = $msz->dbConn->prepare('INSERT INTO msz_news_categories (category_name, category_description, category_is_hidden) VALUES (?, ?, ?)');
 $ncIds = [];
 
 for($i = 0; $i < 10; ++$i) {
@@ -370,7 +367,7 @@ for($i = 0; $i < 10; ++$i) {
     $nc->addParameter(2, mb_substr($newsCatsDescs->generate(), 0, 60000, 'utf-8'));
     $nc->addParameter(3, mt_rand(0, 1));
     $nc->execute();
-    $ncIds[] = $dbConn->getLastInsertId();
+    $ncIds[] = $msz->dbConn->getLastInsertId();
 }
 
 mkv_log('Opening news post markov dictionaries...');
@@ -378,11 +375,11 @@ $newsPostsTitles = new MarkovDictionary(MKV_DICTS . '/news_posts_titles.fmk');
 $newsPostsTexts = new MarkovDictionary(MKV_DICTS . '/news_posts_texts.fmk');
 
 mkv_log('Nuking news posts table...');
-$dbConn->execute('DELETE FROM msz_news_posts');
-$dbConn->execute('ALTER TABLE msz_news_posts AUTO_INCREMENT = 1');
+$msz->dbConn->execute('DELETE FROM msz_news_posts');
+$msz->dbConn->execute('ALTER TABLE msz_news_posts AUTO_INCREMENT = 1');
 
 mkv_log('Preparing news posts table...');
-$np = $dbConn->prepare('INSERT INTO msz_news_posts (category_id, user_id, post_is_featured, post_title, post_text) VALUES (?, ?, ?, ?, ?)');
+$np = $msz->dbConn->prepare('INSERT INTO msz_news_posts (category_id, user_id, post_is_featured, post_title, post_text) VALUES (?, ?, ?, ?, ?)');
 
 for($i = 0; $i < 200; ++$i) {
     mkv_log('Creating bogus news post ' . $i . '...');
@@ -400,18 +397,18 @@ $forumCatsNames = new MarkovDictionary(MKV_DICTS . '/forums_cats_names.fmk');
 $forumCatsDescs = new MarkovDictionary(MKV_DICTS . '/forums_cats_descs.fmk');
 
 mkv_log('Nuking forum category table...');
-$dbConn->execute('DELETE FROM msz_forum_categories');
-$dbConn->execute('ALTER TABLE msz_forum_categories AUTO_INCREMENT = 1');
+$msz->dbConn->execute('DELETE FROM msz_forum_categories');
+$msz->dbConn->execute('ALTER TABLE msz_forum_categories AUTO_INCREMENT = 1');
 
 mkv_log('Inserting 5 root categories and permissions...');
 for($i = 0; $i < 5; ++$i) {
     mkv_log('Inserting bogus category ' . $i . '...');
-    $ic = $dbConn->prepare('INSERT INTO msz_forum_categories (forum_name, forum_type) VALUES (?, 1)');
+    $ic = $msz->dbConn->prepare('INSERT INTO msz_forum_categories (forum_name, forum_type) VALUES (?, 1)');
     $ic->addParameter(1, mb_substr($forumCatsNames->generate(), 0, 240, 'utf-8'));
     $ic->execute();
 
     mkv_log('Inserting permissions for bogus category ' . $i . '...');
-    $ip = $dbConn->prepare('INSERT INTO msz_forum_permissions (forum_id, forum_perms_allow) VALUES (?, 3)');
+    $ip = $msz->dbConn->prepare('INSERT INTO msz_forum_permissions (forum_id, forum_perms_allow) VALUES (?, 3)');
     $ip->addParameter(1, $i + 1);
     $ip->execute();
 }
@@ -422,23 +419,23 @@ mkv_log('Inserting ' . $categories . ' forum sections...');
 $catIds = [];
 for($i = 0; $i < $categories; ++$i) {
     mkv_log('Inserting bogus forum section ' . $i . '...');
-    $ic = $dbConn->prepare('INSERT INTO msz_forum_categories (forum_name, forum_type, forum_description, forum_parent) VALUES (?, 0, ?, ?)');
+    $ic = $msz->dbConn->prepare('INSERT INTO msz_forum_categories (forum_name, forum_type, forum_description, forum_parent) VALUES (?, 0, ?, ?)');
     $ic->addParameter(1, mb_substr($forumCatsNames->generate(), 0, 240, 'utf-8'));
     $ic->addParameter(2, mb_substr($forumCatsDescs->generate(), 0, 1200, 'utf-8'));
     $ic->addParameter(3, mt_rand(1, 5));
     $ic->execute();
-    $catIds[] = $dbConn->getLastInsertId();
+    $catIds[] = $msz->dbConn->getLastInsertId();
 }
 
 mkv_log('Opening forum topic title markov dictionary...');
 $forumTopicsTitles = new MarkovDictionary(MKV_DICTS . '/forums_topics_titles.fmk');
 
 mkv_log('Nuking forum topics table...');
-$dbConn->execute('DELETE FROM msz_forum_topics');
-$dbConn->execute('ALTER TABLE msz_forum_topics AUTO_INCREMENT = 1');
+$msz->dbConn->execute('DELETE FROM msz_forum_topics');
+$msz->dbConn->execute('ALTER TABLE msz_forum_topics AUTO_INCREMENT = 1');
 
 mkv_log('Preparing forum topic insertion statement...');
-$ft = $dbConn->prepare('INSERT INTO msz_forum_topics (forum_id, user_id, topic_type, topic_title, topic_count_views, topic_created, topic_bumped, topic_deleted, topic_locked) VALUES (?, ?, ?, ?, ?, FROM_UNIXTIME(?), FROM_UNIXTIME(?), FROM_UNIXTIME(?), FROM_UNIXTIME(?))');
+$ft = $msz->dbConn->prepare('INSERT INTO msz_forum_topics (forum_id, user_id, topic_type, topic_title, topic_count_views, topic_created, topic_bumped, topic_deleted, topic_locked) VALUES (?, ?, ?, ?, ?, FROM_UNIXTIME(?), FROM_UNIXTIME(?), FROM_UNIXTIME(?), FROM_UNIXTIME(?))');
 
 $topics = mt_rand(200, 2000);
 mkv_log('Creating ' . $topics . ' bogus forum topics...');
@@ -463,18 +460,18 @@ for($i = 0; $i < $topics; ++$i) {
     $ft->addParameter(8, mt_rand(0, 10000) > 9999 ? mt_rand(1, 0x7FFFFFFF) : null);
     $ft->addParameter(9, mt_rand(0, 10000) > 9900 ? mt_rand(1, 0x7FFFFFFF) : null);
     $ft->execute();
-    $topIds[] = $dbConn->getLastInsertId();
+    $topIds[] = $msz->dbConn->getLastInsertId();
 }
 
 mkv_log('Opening forum post text markov dictionary...');
 $forumPostsTexts = new MarkovDictionary(MKV_DICTS . '/forums_posts_texts.fmk');
 
 mkv_log('Nuking forum posts table...');
-$dbConn->execute('DELETE FROM msz_forum_posts');
-$dbConn->execute('ALTER TABLE msz_forum_posts AUTO_INCREMENT = 1');
+$msz->dbConn->execute('DELETE FROM msz_forum_posts');
+$msz->dbConn->execute('ALTER TABLE msz_forum_posts AUTO_INCREMENT = 1');
 
 mkv_log('Preparing forum post insertion statement...');
-$fp = $dbConn->prepare('INSERT INTO msz_forum_posts (topic_id, forum_id, user_id, post_ip, post_text, post_parse, post_display_signature, post_created, post_edited, post_deleted) VALUES (?, 1, ?, ?, ?, ?, ?, FROM_UNIXTIME(?), FROM_UNIXTIME(?), FROM_UNIXTIME(?))');
+$fp = $msz->dbConn->prepare('INSERT INTO msz_forum_posts (topic_id, forum_id, user_id, post_ip, post_text, post_parse, post_display_signature, post_created, post_edited, post_deleted) VALUES (?, 1, ?, ?, ?, ?, ?, FROM_UNIXTIME(?), FROM_UNIXTIME(?), FROM_UNIXTIME(?))');
 
 $topCount = count($topIds);
 for($t = 0; $t < $topCount; ++$t) {
diff --git a/tools/recalc-perms b/tools/recalc-perms
index 390e38e1..8953beb1 100755
--- a/tools/recalc-perms
+++ b/tools/recalc-perms
@@ -4,5 +4,5 @@ namespace Misuzu;
 
 require_once __DIR__ . '/../misuzu.php';
 
-$msz->getConfig()->removeValues('perms.needsRecalc');
-$msz->getPerms()->precalculatePermissions($msz->getForumContext()->getCategories());
+$msz->config->removeValues('perms.needsRecalc');
+$msz->perms->precalculatePermissions($msz->forumCtx->categories);