From 0268b65754efa0e252b2ef8d22c9dd8a819bb168 Mon Sep 17 00:00:00 2001
From: Guy Sartorelli <guy.sartorelli@silverstripe.com>
Date: Thu, 22 Aug 2024 14:24:43 +1200
Subject: [PATCH] API Strong typing for the view layer

---
 src/Control/RSS/RSSFeed_Entry.php             |   7 -
 src/Dev/TaskRunner.php                        |   2 +-
 src/Forms/CompositeField.php                  |   2 +-
 src/Forms/Form.php                            |   8 +-
 src/Forms/FormField.php                       |   8 +-
 src/Forms/FormRequestHandler.php              |   2 +-
 src/Forms/NullableField.php                   |   2 +-
 src/Forms/ReadonlyField.php                   |   2 +-
 src/Forms/RequiredFields.php                  |   2 +-
 src/ORM/ArrayList.php                         |   6 +-
 src/ORM/DataList.php                          |   6 +-
 src/ORM/DataObject.php                        |  77 +++----
 src/ORM/DataObjectInterface.php               |   2 +-
 src/ORM/EagerLoadedList.php                   |   2 +-
 src/ORM/FieldType/DBBigInt.php                |   2 +-
 src/ORM/FieldType/DBBoolean.php               |  22 +-
 src/ORM/FieldType/DBClassName.php             |  45 ++--
 src/ORM/FieldType/DBComposite.php             |  97 +++-----
 src/ORM/FieldType/DBCurrency.php              |  32 +--
 src/ORM/FieldType/DBDate.php                  | 103 +++------
 src/ORM/FieldType/DBDatetime.php              | 104 ++++-----
 src/ORM/FieldType/DBDecimal.php               |  57 ++---
 src/ORM/FieldType/DBDouble.php                |   2 +-
 src/ORM/FieldType/DBEnum.php                  |  93 +++-----
 src/ORM/FieldType/DBField.php                 | 211 +++++-------------
 src/ORM/FieldType/DBFloat.php                 |  18 +-
 src/ORM/FieldType/DBForeignKey.php            |  32 +--
 src/ORM/FieldType/DBHTMLText.php              |  78 +++----
 src/ORM/FieldType/DBHTMLVarchar.php           |  50 ++---
 src/ORM/FieldType/DBInt.php                   |  21 +-
 src/ORM/FieldType/DBLocale.php                |  23 +-
 src/ORM/FieldType/DBMoney.php                 |  74 ++----
 src/ORM/FieldType/DBMultiEnum.php             |  24 +-
 src/ORM/FieldType/DBPercentage.php            |  18 +-
 src/ORM/FieldType/DBPolymorphicForeignKey.php |  28 +--
 src/ORM/FieldType/DBPrimaryKey.php            |  41 ++--
 src/ORM/FieldType/DBString.php                |  53 ++---
 src/ORM/FieldType/DBText.php                  |  45 ++--
 src/ORM/FieldType/DBTime.php                  |  50 ++---
 src/ORM/FieldType/DBVarchar.php               |  33 +--
 src/ORM/FieldType/DBYear.php                  |  13 +-
 src/ORM/ListDecorator.php                     |   6 +-
 src/View/ArrayData.php                        |  19 +-
 src/View/Parsers/HTMLValue.php                |   7 +-
 src/View/Parsers/ShortcodeParser.php          |   2 +-
 src/View/SSTemplateParser.peg                 |   2 +-
 src/View/SSTemplateParser.php                 |   9 +-
 src/View/ViewableData.php                     | 195 ++++------------
 src/View/ViewableData_Customised.php          |  29 ++-
 src/View/ViewableData_Debugger.php            |  18 +-
 tests/php/Core/ClassInfoTest.php              |   2 +-
 .../ViewableDataContainsTest/TestObject.php   |   8 +-
 tests/php/ORM/DBFieldTest/TestDataObject.php  |   4 +-
 tests/php/ORM/DBFieldTest/TestDbField.php     |   2 +-
 tests/php/ORM/DBStringTest/MyStringField.php  |   2 +-
 .../MockDynamicAssignmentDBField.php          |   6 +-
 tests/php/View/ArrayDataTest.php              |   2 +-
 tests/php/View/SSViewerTest/TestFixture.php   |  14 +-
 tests/php/View/ViewableDataTest/Castable.php  |   2 +-
 tests/php/View/ViewableDataTest/Caster.php    |   2 +-
 .../View/ViewableDataTest/RequiresCasting.php |   2 +-
 .../View/ViewableDataTest/UnescapedCaster.php |   2 +-
 62 files changed, 625 insertions(+), 1207 deletions(-)

diff --git a/src/Control/RSS/RSSFeed_Entry.php b/src/Control/RSS/RSSFeed_Entry.php
index 5713b84026b..ff6da977f43 100644
--- a/src/Control/RSS/RSSFeed_Entry.php
+++ b/src/Control/RSS/RSSFeed_Entry.php
@@ -17,13 +17,6 @@
  */
 class RSSFeed_Entry extends ViewableData
 {
-    /**
-     * The object that represents the item, it contains all the data.
-     *
-     * @var mixed
-     */
-    protected $failover;
-
     /**
      * Name of the title field of feed entries
      *
diff --git a/src/Dev/TaskRunner.php b/src/Dev/TaskRunner.php
index 216d6b31327..ecd87c1a26b 100644
--- a/src/Dev/TaskRunner.php
+++ b/src/Dev/TaskRunner.php
@@ -236,7 +236,7 @@ public function canInit(): bool
         }
         return count($this->getTaskList()) > 0;
     }
-    
+
     public function providePermissions(): array
     {
         return [
diff --git a/src/Forms/CompositeField.php b/src/Forms/CompositeField.php
index 15d9abb7a22..7c1cab23f10 100644
--- a/src/Forms/CompositeField.php
+++ b/src/Forms/CompositeField.php
@@ -514,7 +514,7 @@ public function makeFieldReadonly($field)
         return false;
     }
 
-    public function debug()
+    public function debug(): string
     {
         $class = static::class;
         $result = "$class ($this->name) <ul>";
diff --git a/src/Forms/Form.php b/src/Forms/Form.php
index 22338c02ce9..7719193bdaf 100644
--- a/src/Forms/Form.php
+++ b/src/Forms/Form.php
@@ -521,7 +521,7 @@ public function setFieldMessage(
         return $this;
     }
 
-    public function castingHelper($field, bool $useFallback = true)
+    public function castingHelper(string $field, bool $useFallback = true): ?string
     {
         // Override casting for field message
         if (strcasecmp($field ?? '', 'Message') === 0 && ($helper = $this->getMessageCastingHelper())) {
@@ -1547,10 +1547,8 @@ public function getData()
      *
      * This is returned when you access a form as $FormObject rather
      * than <% with FormObject %>
-     *
-     * @return DBHTMLText
      */
-    public function forTemplate()
+    public function forTemplate(): string
     {
         if (!$this->canBeCached()) {
             HTTPCacheControlMiddleware::singleton()->disableCache();
@@ -1750,7 +1748,7 @@ public function removeExtraClass($class)
         return $this;
     }
 
-    public function debug()
+    public function debug(): string
     {
         $class = static::class;
         $result = "<h3>$class</h3><ul>";
diff --git a/src/Forms/FormField.php b/src/Forms/FormField.php
index 3c47b5ff92c..fe31dc6add2 100644
--- a/src/Forms/FormField.php
+++ b/src/Forms/FormField.php
@@ -790,7 +790,7 @@ public function securityTokenEnabled()
         return $form->getSecurityToken()->isEnabled();
     }
 
-    public function castingHelper($field, bool $useFallback = true)
+    public function castingHelper(string $field, bool $useFallback = true): ?string
     {
         // Override casting for field message
         if (strcasecmp($field ?? '', 'Message') === 0 && ($helper = $this->getMessageCastingHelper())) {
@@ -1269,7 +1269,7 @@ public function getDescription()
     /**
      * @return string
      */
-    public function debug()
+    public function debug(): string
     {
         $strValue = is_string($this->value) ? $this->value : print_r($this->value, true);
 
@@ -1286,10 +1286,8 @@ public function debug()
     /**
      * This function is used by the template processor. If you refer to a field as a $ variable, it
      * will return the $Field value.
-     *
-     * @return string
      */
-    public function forTemplate()
+    public function forTemplate(): string
     {
         return $this->Field();
     }
diff --git a/src/Forms/FormRequestHandler.php b/src/Forms/FormRequestHandler.php
index 1071f92aba8..a646944a090 100644
--- a/src/Forms/FormRequestHandler.php
+++ b/src/Forms/FormRequestHandler.php
@@ -504,7 +504,7 @@ public function validationResult()
         return $result;
     }
 
-    public function forTemplate()
+    public function forTemplate(): string
     {
         return $this->form->forTemplate();
     }
diff --git a/src/Forms/NullableField.php b/src/Forms/NullableField.php
index e2c35101a19..99669cd0ab8 100644
--- a/src/Forms/NullableField.php
+++ b/src/Forms/NullableField.php
@@ -166,7 +166,7 @@ public function setName($name)
     /**
      * @return string
      */
-    public function debug()
+    public function debug(): string
     {
         $result = sprintf(
             '%s (%s: %s : <span style="color: red">%s</span>) = ',
diff --git a/src/Forms/ReadonlyField.php b/src/Forms/ReadonlyField.php
index ef31f3d7b55..68a496a5d61 100644
--- a/src/Forms/ReadonlyField.php
+++ b/src/Forms/ReadonlyField.php
@@ -56,7 +56,7 @@ public function Type()
         return 'readonly';
     }
 
-    public function castingHelper($field, bool $useFallback = true)
+    public function castingHelper(string $field, bool $useFallback = true): ?string
     {
         // Get dynamic cast for 'Value' field
         if (strcasecmp($field ?? '', 'Value') === 0) {
diff --git a/src/Forms/RequiredFields.php b/src/Forms/RequiredFields.php
index 3f0e055c0b7..9412e6cfbbe 100644
--- a/src/Forms/RequiredFields.php
+++ b/src/Forms/RequiredFields.php
@@ -58,7 +58,7 @@ public function removeValidation()
      * Debug helper
      * @return string
      */
-    public function debug()
+    public function debug(): string
     {
         if (!is_array($this->required)) {
             return false;
diff --git a/src/ORM/ArrayList.php b/src/ORM/ArrayList.php
index ed20230086a..53527089963 100644
--- a/src/ORM/ArrayList.php
+++ b/src/ORM/ArrayList.php
@@ -110,10 +110,8 @@ public function count(): int
 
     /**
      * Returns true if this list has items
-     *
-     * @return bool
      */
-    public function exists()
+    public function exists(): bool
     {
         return !empty($this->items);
     }
@@ -159,7 +157,7 @@ public function each($callback)
         return $this;
     }
 
-    public function debug()
+    public function debug(): string
     {
         $val = "<h2>" . static::class . "</h2><ul>";
         foreach ($this->toNestedArray() as $item) {
diff --git a/src/ORM/DataList.php b/src/ORM/DataList.php
index 72ccacd7113..23c89029de4 100644
--- a/src/ORM/DataList.php
+++ b/src/ORM/DataList.php
@@ -868,7 +868,7 @@ public function each($callback)
         return $this;
     }
 
-    public function debug()
+    public function debug(): string
     {
         $val = "<h2>" . static::class . "</h2><ul>";
         foreach ($this->toNestedArray() as $item) {
@@ -1702,10 +1702,8 @@ public function last()
 
     /**
      * Returns true if this DataList has items
-     *
-     * @return bool
      */
-    public function exists()
+    public function exists(): bool
     {
         return $this->dataQuery->exists();
     }
diff --git a/src/ORM/DataObject.php b/src/ORM/DataObject.php
index 3852d640c0c..f6908686a09 100644
--- a/src/ORM/DataObject.php
+++ b/src/ORM/DataObject.php
@@ -816,10 +816,8 @@ public function defineMethods()
      * Returns true if this object "exists", i.e., has a sensible value.
      * The default behaviour for a DataObject is to return true if
      * the object exists in the database, you can override this in subclasses.
-     *
-     * @return boolean true if this object exists
      */
-    public function exists()
+    public function exists(): bool
     {
         return $this->isInDB();
     }
@@ -2687,7 +2685,7 @@ public function getFrontEndFields($params = null)
         return $untabbedFields;
     }
 
-    public function getViewerTemplates($suffix = '')
+    public function getViewerTemplates(string $suffix = ''): array
     {
         return SSViewer::get_templates_by_class(static::class, $suffix, $this->baseClass());
     }
@@ -2695,11 +2693,8 @@ public function getViewerTemplates($suffix = '')
     /**
      * Gets the value of a field.
      * Called by {@link __get()} and any getFieldName() methods you might create.
-     *
-     * @param string $field The name of the field
-     * @return mixed The field value
      */
-    public function getField($field)
+    public function getField(string $field): mixed
     {
         // If we already have a value in $this->record, then we should just return that
         if (isset($this->record[$field])) {
@@ -2910,12 +2905,8 @@ public function isChanged($fieldName = null, $changeLevel = DataObject::CHANGE_S
     /**
      * Set the value of the field
      * Called by {@link __set()} and any setFieldName() methods you might create.
-     *
-     * @param string $fieldName Name of the field
-     * @param mixed $val New field value
-     * @return $this
      */
-    public function setField($fieldName, $val)
+    public function setField(string $fieldName, mixed $value): static
     {
         $this->objCacheClear();
         //if it's a has_one component, destroy the cache
@@ -2934,42 +2925,42 @@ public function setField($fieldName, $val)
         if ($schema->unaryComponent(static::class, $fieldName)) {
             unset($this->components[$fieldName]);
             // Assign component directly
-            if (is_null($val) || $val instanceof DataObject) {
-                return $this->setComponent($fieldName, $val);
+            if (is_null($value) || $value instanceof DataObject) {
+                return $this->setComponent($fieldName, $value);
             }
             // Assign by ID instead of object
-            if (is_numeric($val)) {
+            if (is_numeric($value)) {
                 $fieldName .= 'ID';
             }
         }
 
         // Situation 1: Passing an DBField
-        if ($val instanceof DBField) {
-            $val->setName($fieldName);
-            $val->saveInto($this);
+        if ($value instanceof DBField) {
+            $value->setName($fieldName);
+            $value->saveInto($this);
 
             // Situation 1a: Composite fields should remain bound in case they are
             // later referenced to update the parent dataobject
-            if ($val instanceof DBComposite) {
-                $val->bindTo($this);
-                $this->setFieldValue($fieldName, $val);
+            if ($value instanceof DBComposite) {
+                $value->bindTo($this);
+                $this->setFieldValue($fieldName, $value);
             }
         // Situation 2: Passing a literal or non-DBField object
         } else {
-            $this->setFieldValue($fieldName, $val);
+            $this->setFieldValue($fieldName, $value);
         }
         return $this;
     }
 
-    private function setFieldValue(string $fieldName, mixed $val): void
+    private function setFieldValue(string $fieldName, mixed $value): void
     {
         $schema = static::getSchema();
         // If this is a proper database field, we shouldn't be getting non-DBField objects
-        if (is_object($val) && !($val instanceof DBField) && $schema->fieldSpec(static::class, $fieldName)) {
+        if (is_object($value) && !($value instanceof DBField) && $schema->fieldSpec(static::class, $fieldName)) {
             throw new InvalidArgumentException('DataObject::setFieldValue: passed an object that is not a DBField');
         }
 
-        if (!empty($val) && !is_scalar($val)) {
+        if (!empty($value) && !is_scalar($value)) {
             $dbField = $this->dbObject($fieldName);
             if ($dbField && $dbField->scalarValueOnly()) {
                 throw new InvalidArgumentException(
@@ -2982,12 +2973,12 @@ private function setFieldValue(string $fieldName, mixed $val): void
         }
 
         // if a field is not existing or has strictly changed
-        if (!array_key_exists($fieldName, $this->original ?? []) || $this->original[$fieldName] !== $val) {
+        if (!array_key_exists($fieldName, $this->original ?? []) || $this->original[$fieldName] !== $value) {
             // At the very least, the type has changed
             $this->changed[$fieldName] = DataObject::CHANGE_STRICT;
 
-            if ((!array_key_exists($fieldName, $this->original ?? []) && $val)
-                || (array_key_exists($fieldName, $this->original ?? []) && $this->original[$fieldName] != $val)
+            if ((!array_key_exists($fieldName, $this->original ?? []) && $value)
+                || (array_key_exists($fieldName, $this->original ?? []) && $this->original[$fieldName] != $value)
             ) {
                 // Value has changed as well, not just the type
                 $this->changed[$fieldName] = DataObject::CHANGE_VALUE;
@@ -2998,7 +2989,7 @@ private function setFieldValue(string $fieldName, mixed $val): void
         }
 
         // Value is saved regardless, since the change detection relates to the last write
-        $this->record[$fieldName] = $val;
+        $this->record[$fieldName] = $value;
     }
 
     /**
@@ -3029,7 +3020,7 @@ public function setCastedField($fieldName, $value)
     /**
      * {@inheritdoc}
      */
-    public function castingHelper($field, bool $useFallback = true)
+    public function castingHelper(string $field, bool $useFallback = true): ?string
     {
         $fieldSpec = static::getSchema()->fieldSpec(static::class, $field);
         if ($fieldSpec) {
@@ -3054,19 +3045,16 @@ public function castingHelper($field, bool $useFallback = true)
      * Returns true if the given field exists in a database column on any of
      * the objects tables and optionally look up a dynamic getter with
      * get<fieldName>().
-     *
-     * @param string $field Name of the field
-     * @return boolean True if the given field exists
      */
-    public function hasField($field)
+    public function hasField(string $fieldName): bool
     {
         $schema = static::getSchema();
         return (
-            array_key_exists($field, $this->record ?? [])
-            || array_key_exists($field, $this->components ?? [])
-            || $schema->fieldSpec(static::class, $field)
-            || $schema->unaryComponent(static::class, $field)
-            || $this->hasMethod("get{$field}")
+            array_key_exists($fieldName, $this->record ?? [])
+            || array_key_exists($fieldName, $this->components ?? [])
+            || $schema->fieldSpec(static::class, $fieldName)
+            || $schema->unaryComponent(static::class, $fieldName)
+            || $this->hasMethod("get{$fieldName}")
         );
     }
 
@@ -3214,7 +3202,7 @@ public function canCreate($member = null, $context = [])
      *
      * @return string HTML data representing this object
      */
-    public function debug()
+    public function debug(): string
     {
         $class = static::class;
         $val = "<h3>Database record: {$class}</h3>\n<ul>\n";
@@ -4386,13 +4374,8 @@ public function provideI18nEntities()
     /**
      * Returns true if the given method/parameter has a value
      * (Uses the DBField::hasValue if the parameter is a database field)
-     *
-     * @param string $field The field name
-     * @param array $arguments
-     * @param bool $cache
-     * @return boolean
      */
-    public function hasValue($field, $arguments = null, $cache = true)
+    public function hasValue(string $field, array $arguments = [], bool $cache = true): bool
     {
         // has_one fields should not use dbObject to check if a value is given
         $hasOne = static::getSchema()->hasOneComponent(static::class, $field);
diff --git a/src/ORM/DataObjectInterface.php b/src/ORM/DataObjectInterface.php
index b4f43d5dfb9..eba6f022d5e 100644
--- a/src/ORM/DataObjectInterface.php
+++ b/src/ORM/DataObjectInterface.php
@@ -36,7 +36,7 @@ public function delete();
      * @param string $fieldName
      * @return mixed
      */
-    public function __get($fieldName);
+    public function __get(string $property): mixed;
 
     /**
      * Save content from a form into a field on this data object.
diff --git a/src/ORM/EagerLoadedList.php b/src/ORM/EagerLoadedList.php
index 8a53c88905d..e006698007c 100644
--- a/src/ORM/EagerLoadedList.php
+++ b/src/ORM/EagerLoadedList.php
@@ -341,7 +341,7 @@ public function each($callback): static
         return $this;
     }
 
-    public function debug()
+    public function debug(): string
     {
         // Same implementation as DataList::debug()
         $val = '<h2>' . static::class . '</h2><ul>';
diff --git a/src/ORM/FieldType/DBBigInt.php b/src/ORM/FieldType/DBBigInt.php
index 8d427625cd4..c92c2da69df 100644
--- a/src/ORM/FieldType/DBBigInt.php
+++ b/src/ORM/FieldType/DBBigInt.php
@@ -15,7 +15,7 @@
 class DBBigInt extends DBInt
 {
 
-    public function requireField()
+    public function requireField(): void
     {
         $parts = [
             'datatype' => 'bigint',
diff --git a/src/ORM/FieldType/DBBoolean.php b/src/ORM/FieldType/DBBoolean.php
index 46374682cc1..c97eccd3d6b 100644
--- a/src/ORM/FieldType/DBBoolean.php
+++ b/src/ORM/FieldType/DBBoolean.php
@@ -4,21 +4,23 @@
 
 use SilverStripe\Forms\CheckboxField;
 use SilverStripe\Forms\DropdownField;
+use SilverStripe\Forms\FormField;
 use SilverStripe\ORM\DB;
+use SilverStripe\View\ViewableData;
 
 /**
  * Represents a boolean field.
  */
 class DBBoolean extends DBField
 {
-    public function __construct($name = null, $defaultVal = 0)
+    public function __construct(?string $name = null, bool|int $defaultVal = 0)
     {
         $this->defaultVal = ($defaultVal) ? 1 : 0;
 
         parent::__construct($name);
     }
 
-    public function requireField()
+    public function requireField(): void
     {
         $parts = [
             'datatype' => 'tinyint',
@@ -32,17 +34,17 @@ public function requireField()
         DB::require_field($this->tableName, $this->name, $values);
     }
 
-    public function Nice()
+    public function Nice(): string
     {
         return ($this->value) ? _t(__CLASS__ . '.YESANSWER', 'Yes') : _t(__CLASS__ . '.NOANSWER', 'No');
     }
 
-    public function NiceAsBoolean()
+    public function NiceAsBoolean(): string
     {
         return ($this->value) ? 'true' : 'false';
     }
 
-    public function saveInto($dataObject)
+    public function saveInto(ViewableData $dataObject): void
     {
         $fieldName = $this->name;
         if ($fieldName) {
@@ -57,12 +59,12 @@ public function saveInto($dataObject)
         }
     }
 
-    public function scaffoldFormField($title = null, $params = null)
+    public function scaffoldFormField(?string $title = null, array $params = []): ?FormField
     {
         return CheckboxField::create($this->name, $title);
     }
 
-    public function scaffoldSearchField($title = null)
+    public function scaffoldSearchField(?string $title = null): ?FormField
     {
         $anyText = _t(__CLASS__ . '.ANY', 'Any');
         $source = [
@@ -71,16 +73,16 @@ public function scaffoldSearchField($title = null)
             0 => _t(__CLASS__ . '.NOANSWER', 'No')
         ];
 
-        return (new DropdownField($this->name, $title, $source))
+        return DropdownField::create($this->name, $title, $source)
             ->setEmptyString($anyText);
     }
 
-    public function nullValue()
+    public function nullValue(): ?int
     {
         return 0;
     }
 
-    public function prepValueForDB($value)
+    public function prepValueForDB(mixed $value): ?int
     {
         if (is_bool($value)) {
             return $value ? 1 : 0;
diff --git a/src/ORM/FieldType/DBClassName.php b/src/ORM/FieldType/DBClassName.php
index 4848b05734e..d8008626bca 100644
--- a/src/ORM/FieldType/DBClassName.php
+++ b/src/ORM/FieldType/DBClassName.php
@@ -6,48 +6,40 @@
 use SilverStripe\Core\Config\Config;
 use SilverStripe\ORM\DataObject;
 use SilverStripe\ORM\DB;
+use SilverStripe\View\ViewableData;
 
 /**
  * Represents a classname selector, which respects obsolete clasess.
  */
 class DBClassName extends DBEnum
 {
-
     /**
      * Base classname of class to enumerate.
      * If 'DataObject' then all classes are included.
      * If empty, then the baseClass of the parent object will be used
-     *
-     * @var string|null
      */
-    protected $baseClass = null;
+    protected ?string $baseClass = null;
 
     /**
      * Parent object
-     *
-     * @var DataObject|null
      */
-    protected $record = null;
+    protected ?DataObject $record = null;
 
-    private static $index = true;
+    private static string|bool $index = true;
 
     /**
      * Create a new DBClassName field
      *
-     * @param string      $name      Name of field
      * @param string|null $baseClass Optional base class to limit selections
-     * @param array       $options   Optional parameters for this DBField instance
+     * @param array $options Optional parameters for this DBField instance
      */
-    public function __construct($name = null, $baseClass = null, $options = [])
+    public function __construct(?string $name = null, ?string $baseClass = null, array $options = [])
     {
         $this->setBaseClass($baseClass);
         parent::__construct($name, null, null, $options);
     }
 
-    /**
-     * @return void
-     */
-    public function requireField()
+    public function requireField(): void
     {
         $parts = [
             'datatype' => 'enum',
@@ -69,10 +61,8 @@ public function requireField()
 
     /**
      * Get the base dataclass for the list of subclasses
-     *
-     * @return string
      */
-    public function getBaseClass()
+    public function getBaseClass(): string
     {
         // Use explicit base class
         if ($this->baseClass) {
@@ -95,25 +85,20 @@ public function getBaseClass()
     /**
      * Get the base name of the current class
      * Useful as a non-fully qualified CSS Class name in templates.
-     *
-     * @return string|null
      */
-    public function getShortName()
+    public function getShortName(): string
     {
         $value = $this->getValue();
         if (empty($value) || !ClassInfo::exists($value)) {
-            return null;
+            return '';
         }
         return ClassInfo::shortName($value);
     }
 
     /**
      * Assign the base class
-     *
-     * @param string $baseClass
-     * @return $this
      */
-    public function setBaseClass($baseClass)
+    public function setBaseClass(?string $baseClass): static
     {
         $this->baseClass = $baseClass;
         return $this;
@@ -121,10 +106,8 @@ public function setBaseClass($baseClass)
 
     /**
      * Get list of classnames that should be selectable
-     *
-     * @return array
      */
-    public function getEnum()
+    public function getEnum(): array
     {
         $classNames = ClassInfo::subclassesFor($this->getBaseClass());
         $dataobject = strtolower(DataObject::class);
@@ -132,7 +115,7 @@ public function getEnum()
         return array_values($classNames ?? []);
     }
 
-    public function setValue($value, $record = null, $markChanged = true)
+    public function setValue(mixed $value, null|array|ViewableData $record = null, bool $markChanged = true): static
     {
         parent::setValue($value, $record, $markChanged);
 
@@ -143,7 +126,7 @@ public function setValue($value, $record = null, $markChanged = true)
         return $this;
     }
 
-    public function getDefault()
+    public function getDefault(): string
     {
         // Check for assigned default
         $default = parent::getDefault();
diff --git a/src/ORM/FieldType/DBComposite.php b/src/ORM/FieldType/DBComposite.php
index 1fa7001db30..568eb5bf67a 100644
--- a/src/ORM/FieldType/DBComposite.php
+++ b/src/ORM/FieldType/DBComposite.php
@@ -7,6 +7,7 @@
 use SilverStripe\ORM\DataObject;
 use SilverStripe\ORM\DB;
 use SilverStripe\ORM\Queries\SQLSelect;
+use SilverStripe\View\ViewableData;
 
 /**
  * Extend this class when designing a {@link DBField} that doesn't have a 1-1 mapping with a database field.
@@ -29,26 +30,21 @@ abstract class DBComposite extends DBField
      * holds an array of composite field names.
      * Don't include the fields "main name",
      * it will be prefixed in {@link requireField()}.
-     *
-     * @config
-     * @var array
      */
-    private static $composite_db = [];
+    private static array $composite_db = [];
 
     /**
      * Marker as to whether this record has changed
      * Only used when deference to the parent object isn't possible
      */
-    protected $isChanged = false;
+    protected bool $isChanged = false;
 
     /**
      * Either the parent dataobject link, or a record of saved values for each field
-     *
-     * @var array|DataObject
      */
-    protected $record = [];
+    protected array|ViewableData $record = [];
 
-    public function __set($property, $value)
+    public function __set(string $property, mixed $value): void
     {
         // Prevent failover / extensions from hijacking composite field setters
         // by intentionally avoiding hasMethod()
@@ -59,7 +55,7 @@ public function __set($property, $value)
         parent::__set($property, $value);
     }
 
-    public function __get($property)
+    public function __get(string $property): mixed
     {
         // Prevent failover / extensions from hijacking composite field getters
         // by intentionally avoiding hasMethod()
@@ -71,10 +67,8 @@ public function __get($property)
 
     /**
      * Write all nested fields into a manipulation
-     *
-     * @param array $manipulation
      */
-    public function writeToManipulation(&$manipulation)
+    public function writeToManipulation(array &$manipulation): void
     {
         foreach ($this->compositeDatabaseFields() as $field => $spec) {
             // Write sub-manipulation
@@ -88,10 +82,8 @@ public function writeToManipulation(&$manipulation)
      * and {@link $composite_db}, or any additional SQL that is required
      * to get to these columns. Will mostly just write to the {@link SQLSelect->select}
      * array.
-     *
-     * @param SQLSelect $query
      */
-    public function addToQuery(&$query)
+    public function addToQuery(SQLSelect &$query): void
     {
         parent::addToQuery($query);
 
@@ -109,12 +101,10 @@ public function addToQuery(&$query)
     /**
      * Return array in the format of {@link $composite_db}.
      * Used by {@link DataObject->hasOwnDatabaseField()}.
-     *
-     * @return array
      */
-    public function compositeDatabaseFields()
+    public function compositeDatabaseFields(): array
     {
-        return $this->config()->composite_db;
+        return static::config()->get('composite_db');
     }
 
 
@@ -122,7 +112,7 @@ public function compositeDatabaseFields()
      * Returns true if this composite field has changed.
      * For fields bound to a DataObject, this will be cleared when the DataObject is written.
      */
-    public function isChanged()
+    public function isChanged(): bool
     {
         // When unbound, use the local changed flag
         if (!$this->record instanceof DataObject) {
@@ -141,10 +131,8 @@ public function isChanged()
 
     /**
      * Composite field defaults to exists only if all fields have values
-     *
-     * @return boolean
      */
-    public function exists()
+    public function exists(): bool
     {
         // By default all fields
         foreach ($this->compositeDatabaseFields() as $field => $spec) {
@@ -156,7 +144,7 @@ public function exists()
         return true;
     }
 
-    public function requireField()
+    public function requireField(): void
     {
         foreach ($this->compositeDatabaseFields() as $field => $spec) {
             $key = $this->getName() . $field;
@@ -171,12 +159,9 @@ public function requireField()
      *
      * {@see ViewableData::obj}
      *
-     * @param mixed $value
-     * @param mixed $record Parent object to this field, which could be a DataObject, record array, or other
-     * @param bool $markChanged
-     * @return $this
+     * @param null|array|ViewableData $record Parent object to this field, which could be a DataObject, record array, or other
      */
-    public function setValue($value, $record = null, $markChanged = true)
+    public function setValue(mixed $value, null|array|ViewableData $record = null, bool $markChanged = true): static
     {
         $this->isChanged = $markChanged;
 
@@ -206,16 +191,14 @@ public function setValue($value, $record = null, $markChanged = true)
     }
 
     /**
-     * Bind this field to the dataobject, and set the underlying table to that of the owner
-     *
-     * @param DataObject $dataObject
+     * Bind this field to the model, and set the underlying table to that of the owner
      */
-    public function bindTo($dataObject)
+    public function bindTo(DataObject $dataObject): void
     {
         $this->record = $dataObject;
     }
 
-    public function saveInto($dataObject)
+    public function saveInto(ViewableData $dataObject): void
     {
         foreach ($this->compositeDatabaseFields() as $field => $spec) {
             // Save into record
@@ -230,52 +213,44 @@ public function saveInto($dataObject)
 
     /**
      * get value of a single composite field
-     *
-     * @param string $field
-     * @return mixed
      */
-    public function getField($field)
+    public function getField(string $fieldName): mixed
     {
         // Skip invalid fields
         $fields = $this->compositeDatabaseFields();
-        if (!isset($fields[$field])) {
+        if (!isset($fields[$fieldName])) {
             return null;
         }
 
         // Check bound object
         if ($this->record instanceof DataObject) {
-            $key = $this->getName() . $field;
+            $key = $this->getName() . $fieldName;
             return $this->record->getField($key);
         }
 
         // Check local record
-        if (isset($this->record[$field])) {
-            return $this->record[$field];
+        if (isset($this->record[$fieldName])) {
+            return $this->record[$fieldName];
         }
         return null;
     }
 
-    public function hasField($field)
+    public function hasField(string $fieldName): bool
     {
         $fields = $this->compositeDatabaseFields();
-        return isset($fields[$field]);
+        return isset($fields[$fieldName]);
     }
 
     /**
      * Set value of a single composite field
-     *
-     * @param string $field
-     * @param mixed $value
-     * @param bool $markChanged
-     * @return $this
      */
-    public function setField($field, $value, $markChanged = true)
+    public function setField(string $fieldName, mixed $value, bool $markChanged = true): static
     {
         $this->objCacheClear();
 
-        if (!$this->hasField($field)) {
+        if (!$this->hasField($fieldName)) {
             throw new InvalidArgumentException(implode(' ', [
-                "Field $field does not exist.",
+                "Field $fieldName does not exist.",
                 'If this was accessed via a dynamic property then call setDynamicData() instead.'
             ]));
         }
@@ -287,23 +262,20 @@ public function setField($field, $value, $markChanged = true)
 
         // Set bound object
         if ($this->record instanceof DataObject) {
-            $key = $this->getName() . $field;
+            $key = $this->getName() . $fieldName;
             $this->record->setField($key, $value);
             return $this;
         }
 
         // Set local record
-        $this->record[$field] = $value;
+        $this->record[$fieldName] = $value;
         return $this;
     }
 
     /**
      * Get a db object for the named field
-     *
-     * @param string $field Field name
-     * @return DBField|null
      */
-    public function dbObject($field)
+    public function dbObject(string $field): ?DBField
     {
         $fields = $this->compositeDatabaseFields();
         if (!isset($fields[$field])) {
@@ -319,7 +291,7 @@ public function dbObject($field)
         return $fieldObject;
     }
 
-    public function castingHelper($field, bool $useFallback = true)
+    public function castingHelper(string $field, bool $useFallback = true): ?string
     {
         $fields = $this->compositeDatabaseFields();
         if (isset($fields[$field])) {
@@ -329,7 +301,7 @@ public function castingHelper($field, bool $useFallback = true)
         return parent::castingHelper($field, $useFallback);
     }
 
-    public function getIndexSpecs()
+    public function getIndexSpecs(): array
     {
         if ($type = $this->getIndexType()) {
             $columns = array_map(function ($name) {
@@ -341,9 +313,10 @@ public function getIndexSpecs()
                 'columns' => $columns,
             ];
         }
+        return [];
     }
 
-    public function scalarValueOnly()
+    public function scalarValueOnly(): bool
     {
         return false;
     }
diff --git a/src/ORM/FieldType/DBCurrency.php b/src/ORM/FieldType/DBCurrency.php
index 258d427f4b7..20e612365d3 100644
--- a/src/ORM/FieldType/DBCurrency.php
+++ b/src/ORM/FieldType/DBCurrency.php
@@ -3,6 +3,8 @@
 namespace SilverStripe\ORM\FieldType;
 
 use SilverStripe\Forms\CurrencyField;
+use SilverStripe\Forms\FormField;
+use SilverStripe\View\ViewableData;
 
 /**
  * Represents a decimal field containing a currency amount.
@@ -20,22 +22,16 @@
 class DBCurrency extends DBDecimal
 {
     /**
-     * @config
-     * @var string
+     * The symbol that represents the currency
      */
-    private static $currency_symbol = '$';
-
-    public function __construct($name = null, $wholeSize = 9, $decimalSize = 2, $defaultValue = 0)
-    {
-        parent::__construct($name, $wholeSize, $decimalSize, $defaultValue);
-    }
+    private static string $currency_symbol = '$';
 
     /**
      * Returns the number as a currency, eg “$1,000.00”.
      */
-    public function Nice()
+    public function Nice(): string
     {
-        $val = $this->config()->currency_symbol . number_format(abs($this->value ?? 0.0) ?? 0.0, 2);
+        $val = static::config()->get('currency_symbol') . number_format(abs($this->value ?? 0.0) ?? 0.0, 2);
         if ($this->value < 0) {
             return "($val)";
         }
@@ -46,22 +42,22 @@ public function Nice()
     /**
      * Returns the number as a whole-number currency, eg “$1,000”.
      */
-    public function Whole()
+    public function Whole(): string
     {
-        $val = $this->config()->currency_symbol . number_format(abs($this->value ?? 0.0) ?? 0.0, 0);
+        $val = static::config()->get('currency_symbol') . number_format(abs($this->value ?? 0.0) ?? 0.0, 0);
         if ($this->value < 0) {
             return "($val)";
         }
         return $val;
     }
 
-    public function setValue($value, $record = null, $markChanged = true)
+    public function setValue(mixed $value, null|array|ViewableData $record = null, bool $markChanged = true): static
     {
         $matches = null;
         if (is_numeric($value)) {
             $this->value = $value;
         } elseif (preg_match('/-?\$?[0-9,]+(.[0-9]+)?([Ee][0-9]+)?/', $value ?? '', $matches)) {
-            $this->value = str_replace(['$', ',', $this->config()->currency_symbol], '', $matches[0] ?? '');
+            $this->value = str_replace(['$', ',', static::config()->get('currency_symbol')], '', $matches[0] ?? '');
         } else {
             $this->value = 0;
         }
@@ -69,13 +65,7 @@ public function setValue($value, $record = null, $markChanged = true)
         return $this;
     }
 
-    /**
-     * @param string $title
-     * @param array $params
-     *
-     * @return CurrencyField
-     */
-    public function scaffoldFormField($title = null, $params = null)
+    public function scaffoldFormField(?string $title = null, array $params = []): ?FormField
     {
         return CurrencyField::create($this->getName(), $title);
     }
diff --git a/src/ORM/FieldType/DBDate.php b/src/ORM/FieldType/DBDate.php
index a035d018884..40dcbaac963 100644
--- a/src/ORM/FieldType/DBDate.php
+++ b/src/ORM/FieldType/DBDate.php
@@ -6,10 +6,12 @@
 use InvalidArgumentException;
 use NumberFormatter;
 use SilverStripe\Forms\DateField;
+use SilverStripe\Forms\FormField;
 use SilverStripe\i18n\i18n;
 use SilverStripe\ORM\DB;
 use SilverStripe\Security\Member;
 use SilverStripe\Security\Security;
+use SilverStripe\View\ViewableData;
 
 /**
  * Represents a date field.
@@ -32,15 +34,15 @@ class DBDate extends DBField
     /**
      * Standard ISO format string for date in CLDR standard format
      */
-    const ISO_DATE = 'y-MM-dd';
+    public const ISO_DATE = 'y-MM-dd';
 
     /**
      * Fixed locale to use for ISO date formatting. This is necessary to prevent
      * locale-specific numeric localisation breaking internal date strings.
      */
-    const ISO_LOCALE = 'en_US';
+    public const ISO_LOCALE = 'en_US';
 
-    public function setValue($value, $record = null, $markChanged = true)
+    public function setValue(mixed $value, null|array|ViewableData $record = null, bool $markChanged = true): static
     {
         $value = $this->parseDate($value);
         if ($value === false) {
@@ -58,7 +60,7 @@ public function setValue($value, $record = null, $markChanged = true)
      * @param mixed $value
      * @return string|null|false Formatted date, null if empty but valid, or false if invalid
      */
-    protected function parseDate($value)
+    protected function parseDate(mixed $value): string|null|false
     {
         // Skip empty values
         if (empty($value) && !is_numeric($value)) {
@@ -89,13 +91,11 @@ protected function parseDate($value)
 
     /**
      * Returns the standard localised medium date
-     *
-     * @return ?string
      */
-    public function Nice()
+    public function Nice(): string
     {
         if (!$this->value) {
-            return null;
+            return '';
         }
         $formatter = $this->getFormatter();
         return $formatter->format($this->getTimestamp());
@@ -103,40 +103,32 @@ public function Nice()
 
     /**
      * Returns the year from the given date
-     *
-     * @return string
      */
-    public function Year()
+    public function Year(): string
     {
         return $this->Format('y');
     }
 
     /**
      * Returns the day of the week
-     *
-     * @return string
      */
-    public function DayOfWeek()
+    public function DayOfWeek(): string
     {
         return $this->Format('cccc');
     }
 
     /**
      * Returns a full textual representation of a month, such as January.
-     *
-     * @return string
      */
-    public function Month()
+    public function Month(): string
     {
         return $this->Format('LLLL');
     }
 
     /**
      * Returns the short version of the month such as Jan
-     *
-     * @return string
      */
-    public function ShortMonth()
+    public function ShortMonth(): string
     {
         return $this->Format('LLL');
     }
@@ -145,9 +137,8 @@ public function ShortMonth()
      * Returns the day of the month.
      *
      * @param bool $includeOrdinal Include ordinal suffix to day, e.g. "th" or "rd"
-     * @return string
      */
-    public function DayOfMonth($includeOrdinal = false)
+    public function DayOfMonth(bool $includeOrdinal = false): string
     {
         $number = $this->Format('d');
         if ($includeOrdinal && $number) {
@@ -159,13 +150,11 @@ public function DayOfMonth($includeOrdinal = false)
 
     /**
      * Returns the date in the localised short format
-     *
-     * @return string
      */
-    public function Short()
+    public function Short(): string
     {
         if (!$this->value) {
-            return null;
+            return '';
         }
         $formatter = $this->getFormatter(IntlDateFormatter::SHORT);
         return $formatter->format($this->getTimestamp());
@@ -173,13 +162,11 @@ public function Short()
 
     /**
      * Returns the date in the localised long format
-     *
-     * @return string
      */
-    public function Long()
+    public function Long(): string
     {
         if (!$this->value) {
-            return null;
+            return '';
         }
         $formatter = $this->getFormatter(IntlDateFormatter::LONG);
         return $formatter->format($this->getTimestamp());
@@ -187,13 +174,11 @@ public function Long()
 
     /**
      * Returns the date in the localised full format
-     *
-     * @return string
      */
-    public function Full()
+    public function Full(): string
     {
         if (!$this->value) {
-            return null;
+            return '';
         }
         $formatter = $this->getFormatter(IntlDateFormatter::FULL);
         return $formatter->format($this->getTimestamp());
@@ -201,12 +186,8 @@ public function Full()
 
     /**
      * Get date formatter
-     *
-     * @param int $dateLength
-     * @param int $timeLength
-     * @return IntlDateFormatter
      */
-    public function getFormatter($dateLength = IntlDateFormatter::MEDIUM, $timeLength = IntlDateFormatter::NONE)
+    public function getFormatter(int $dateLength = IntlDateFormatter::MEDIUM, int $timeLength = IntlDateFormatter::NONE): IntlDateFormatter
     {
         return $this->getCustomFormatter(null, null, $dateLength, $timeLength);
     }
@@ -216,16 +197,13 @@ public function getFormatter($dateLength = IntlDateFormatter::MEDIUM, $timeLengt
      *
      * @param string|null $locale The current locale, or null to use default
      * @param string|null $pattern Custom pattern to use for this, if required
-     * @param int $dateLength
-     * @param int $timeLength
-     * @return IntlDateFormatter
      */
     public function getCustomFormatter(
-        $locale = null,
-        $pattern = null,
-        $dateLength = IntlDateFormatter::MEDIUM,
-        $timeLength = IntlDateFormatter::NONE
-    ) {
+        ?string $locale = null,
+        ?string $pattern = null,
+        int $dateLength = IntlDateFormatter::MEDIUM,
+        int $timeLength = IntlDateFormatter::NONE
+    ): IntlDateFormatter {
         $locale = $locale ?: i18n::get_locale();
         $formatter = IntlDateFormatter::create($locale, $dateLength, $timeLength);
         if ($pattern) {
@@ -238,9 +216,8 @@ public function getCustomFormatter(
      * Formatter used internally
      *
      * @internal
-     * @return IntlDateFormatter
      */
-    protected function getInternalFormatter()
+    protected function getInternalFormatter(): IntlDateFormatter
     {
         $formatter = $this->getCustomFormatter(DBDate::ISO_LOCALE, DBDate::ISO_DATE);
         $formatter->setLenient(false);
@@ -249,10 +226,8 @@ protected function getInternalFormatter()
 
     /**
      * Get standard ISO date format string
-     *
-     * @return string
      */
-    public function getISOFormat()
+    public function getISOFormat(): string
     {
         return DBDate::ISO_DATE;
     }
@@ -262,16 +237,13 @@ public function getISOFormat()
      * for the day of the month ("1st", "2nd", "3rd" etc)
      *
      * @param string $format Format code string. See https://unicode-org.github.io/icu/userguide/format_parse/datetime
-     * @param string $locale Custom locale to use (add to signature in 5.0)
-     * @return ?string The date in the requested format
+     * @param string|null $locale Custom locale to use
+     * @return string The date in the requested format
      */
-    public function Format($format)
+    public function Format(string $format, ?string $locale = null): string
     {
-        // Note: soft-arg uses func_get_args() to respect semver. Add to signature in 5.0
-        $locale = func_num_args() > 1 ? func_get_arg(1) : null;
-
         if (!$this->value) {
-            return null;
+            return '';
         }
 
         // Replace {o} with ordinal representation of day of the month
@@ -285,10 +257,8 @@ public function Format($format)
 
     /**
      * Get unix timestamp for this date
-     *
-     * @return int
      */
-    public function getTimestamp()
+    public function getTimestamp(): int
     {
         if ($this->value) {
             return strtotime($this->value ?? '');
@@ -299,10 +269,9 @@ public function getTimestamp()
     /**
      * Return a date formatted as per a CMS user's settings.
      *
-     * @param Member $member
-     * @return boolean | string A date formatted as per user-defined settings.
+     * @return string A date formatted as per user-defined settings.
      */
-    public function FormatFromSettings($member = null)
+    public function FormatFromSettings(?Member $member = null): string
     {
         if (!$member) {
             $member = Security::getCurrentUser();
@@ -512,7 +481,7 @@ public function TimeDiffIn($format)
         }
     }
 
-    public function requireField()
+    public function requireField(): void
     {
         $parts = ['datatype' => 'date', 'arrayValue' => $this->arrayValue];
         $values = ['type' => 'date', 'parts' => $parts];
@@ -575,7 +544,7 @@ public function URLDate()
         return rawurlencode($this->Format(DBDate::ISO_DATE, DBDate::ISO_LOCALE) ?? '');
     }
 
-    public function scaffoldFormField($title = null, $params = null)
+    public function scaffoldFormField(?string $title = null, array $params = []): ?FormField
     {
         $field = DateField::create($this->name, $title);
         $field->setHTML5(true);
diff --git a/src/ORM/FieldType/DBDatetime.php b/src/ORM/FieldType/DBDatetime.php
index 877c707f260..3f1cbf81581 100644
--- a/src/ORM/FieldType/DBDatetime.php
+++ b/src/ORM/FieldType/DBDatetime.php
@@ -6,10 +6,12 @@
 use IntlDateFormatter;
 use InvalidArgumentException;
 use SilverStripe\Forms\DatetimeField;
+use SilverStripe\Forms\FormField;
 use SilverStripe\ORM\DB;
 use SilverStripe\Security\Member;
 use SilverStripe\Security\Security;
 use SilverStripe\View\TemplateGlobalProvider;
+use SilverStripe\View\ViewableData;
 
 /**
  * Represents a date-time field.
@@ -37,35 +39,36 @@ class DBDatetime extends DBDate implements TemplateGlobalProvider
      * Standard ISO format string for date and time in CLDR standard format,
      * with a whitespace separating date and time (common database representation, e.g. in MySQL).
      */
-    const ISO_DATETIME = 'y-MM-dd HH:mm:ss';
+    public const ISO_DATETIME = 'y-MM-dd HH:mm:ss';
 
     /**
      * Standard ISO format string for date and time in CLDR standard format,
      * with a "T" separator between date and time (W3C standard, e.g. for HTML5 datetime-local fields).
      */
-    const ISO_DATETIME_NORMALISED = 'y-MM-dd\'T\'HH:mm:ss';
+    public const ISO_DATETIME_NORMALISED = 'y-MM-dd\'T\'HH:mm:ss';
 
     /**
      * Flag idicating if this field is considered immutable
      * when this is enabled setting the value of this field will return a new field instance
      * instead updatin the old one
-     *
-     * @var bool
      */
-    protected $immutable = false;
+    protected bool $immutable = false;
 
     /**
-     * @param bool $immutable
-     * @return $this
+     * Used to set a specific time for "now", useful for unit tests.
      */
-    public function setImmutable(bool $immutable): DBDatetime
+    protected static ?DBDatetime $mock_now = null;
+
+    /**
+     * Set whether this field is mutable (can be modified) or immutable (cannot be modified)
+     */
+    public function setImmutable(bool $immutable): static
     {
         $this->immutable = $immutable;
-
         return $this;
     }
 
-    public function setValue($value, $record = null, $markChanged = true)
+    public function setValue(mixed $value, null|array|ViewableData $record = null, bool $markChanged = true): static
     {
         if ($this->immutable) {
             // This field is set as immutable so we have to create a new field instance
@@ -87,10 +90,8 @@ public function setValue($value, $record = null, $markChanged = true)
 
     /**
      * Returns the standard localised date
-     *
-     * @return string Formatted date.
      */
-    public function Date()
+    public function Date(): string
     {
         $formatter = $this->getFormatter(IntlDateFormatter::MEDIUM, IntlDateFormatter::NONE);
         return $formatter->format($this->getTimestamp());
@@ -98,10 +99,8 @@ public function Date()
 
     /**
      * Returns the standard localised time
-     *
-     * @return string Formatted time.
      */
-    public function Time()
+    public function Time(): string
     {
         $formatter = $this->getFormatter(IntlDateFormatter::NONE, IntlDateFormatter::MEDIUM);
         return $formatter->format($this->getTimestamp());
@@ -109,20 +108,16 @@ public function Time()
 
     /**
      * Returns the time in 12-hour format using the format string 'h:mm a' e.g. '1:32 pm'.
-     *
-     * @return string Formatted time.
      */
-    public function Time12()
+    public function Time12(): string
     {
         return $this->Format('h:mm a');
     }
 
     /**
      * Returns the time in 24-hour format using the format string 'H:mm' e.g. '13:32'.
-     *
-     * @return string Formatted time.
      */
-    public function Time24()
+    public function Time24(): string
     {
         return $this->Format('H:mm');
     }
@@ -130,10 +125,9 @@ public function Time24()
     /**
      * Return a date and time formatted as per a CMS user's settings.
      *
-     * @param Member $member
-     * @return boolean|string A time and date pair formatted as per user-defined settings.
+     * @return string A time and date pair formatted as per user-defined settings.
      */
-    public function FormatFromSettings($member = null)
+    public function FormatFromSettings(?Member $member = null): string
     {
         if (!$member) {
             $member = Security::getCurrentUser();
@@ -151,7 +145,7 @@ public function FormatFromSettings($member = null)
         return $this->Format($dateFormat . ' ' . $timeFormat, $member->getLocale());
     }
 
-    public function requireField()
+    public function requireField(): void
     {
         $parts = [
             'datatype' => 'datetime',
@@ -167,15 +161,13 @@ public function requireField()
     /**
      * Returns the url encoded date and time in ISO 6801 format using format
      * string 'y-MM-dd%20HH:mm:ss' e.g. '2014-02-28%2013:32:22'.
-     *
-     * @return string Formatted date and time.
      */
-    public function URLDatetime()
+    public function URLDatetime(): string
     {
         return rawurlencode($this->Format(DBDatetime::ISO_DATETIME, DBDatetime::ISO_LOCALE) ?? '');
     }
 
-    public function scaffoldFormField($title = null, $params = null)
+    public function scaffoldFormField(?string $title = null, array $params = []): ?FormField
     {
         $field = DatetimeField::create($this->name, $title);
         $dateTimeFormat = $field->getDatetimeFormat();
@@ -195,18 +187,11 @@ public function scaffoldFormField($title = null, $params = null)
         return $field;
     }
 
-    /**
-     *
-     */
-    protected static $mock_now = null;
-
     /**
      * Returns either the current system date as determined
      * by date(), or a mocked date through {@link set_mock_now()}.
-     *
-     * @return static
      */
-    public static function now()
+    public static function now(): static
     {
         $time = DBDatetime::$mock_now ? DBDatetime::$mock_now->Value : time();
 
@@ -224,7 +209,7 @@ public static function now()
      * @param DBDatetime|string $datetime Either in object format, or as a DBDatetime compatible string.
      * @throws Exception
      */
-    public static function set_mock_now($datetime)
+    public static function set_mock_now(DBDatetime|string $datetime): void
     {
         if (!$datetime instanceof DBDatetime) {
             $value = $datetime;
@@ -240,20 +225,15 @@ public static function set_mock_now($datetime)
      * Clear any mocked date, which causes
      * {@link Now()} to return the current system date.
      */
-    public static function clear_mock_now()
+    public static function clear_mock_now(): void
     {
         DBDatetime::$mock_now = null;
     }
 
     /**
      * Run a callback with specific time, original mock value is retained after callback
-     *
-     * @param DBDatetime|string $time
-     * @param callable $callback
-     * @return mixed
-     * @throws Exception
      */
-    public static function withFixedNow($time, $callback)
+    public static function withFixedNow(DBDatetime|string $time, callable $callback): mixed
     {
         $original = DBDatetime::$mock_now;
 
@@ -266,7 +246,7 @@ public static function withFixedNow($time, $callback)
         }
     }
 
-    public static function get_template_global_variables()
+    public static function get_template_global_variables(): array
     {
         return [
             'Now' => ['method' => 'now', 'casting' => 'Datetime'],
@@ -275,13 +255,11 @@ public static function get_template_global_variables()
 
     /**
      * Get date / time formatter for the current locale
-     *
-     * @param int $dateLength
-     * @param int $timeLength
-     * @return IntlDateFormatter
      */
-    public function getFormatter($dateLength = IntlDateFormatter::MEDIUM, $timeLength = IntlDateFormatter::SHORT)
-    {
+    public function getFormatter(
+        int $dateLength = IntlDateFormatter::MEDIUM,
+        int $timeLength = IntlDateFormatter::SHORT
+    ): IntlDateFormatter {
         return parent::getFormatter($dateLength, $timeLength);
     }
 
@@ -291,16 +269,13 @@ public function getFormatter($dateLength = IntlDateFormatter::MEDIUM, $timeLengt
      *
      * @param string|null $locale The current locale, or null to use default
      * @param string|null $pattern Custom pattern to use for this, if required
-     * @param int $dateLength
-     * @param int $timeLength
-     * @return IntlDateFormatter
      */
     public function getCustomFormatter(
-        $locale = null,
-        $pattern = null,
-        $dateLength = IntlDateFormatter::MEDIUM,
-        $timeLength = IntlDateFormatter::MEDIUM
-    ) {
+        ?string $locale = null,
+        ?string $pattern = null,
+        int $dateLength = IntlDateFormatter::MEDIUM,
+        int $timeLength = IntlDateFormatter::MEDIUM
+    ): IntlDateFormatter {
         return parent::getCustomFormatter($locale, $pattern, $dateLength, $timeLength);
     }
 
@@ -308,9 +283,8 @@ public function getCustomFormatter(
      * Formatter used internally
      *
      * @internal
-     * @return IntlDateFormatter
      */
-    protected function getInternalFormatter()
+    protected function getInternalFormatter(): IntlDateFormatter
     {
         $formatter = $this->getCustomFormatter(DBDate::ISO_LOCALE, DBDatetime::ISO_DATETIME);
         $formatter->setLenient(false);
@@ -319,10 +293,8 @@ protected function getInternalFormatter()
 
     /**
      * Get standard ISO date format string
-     *
-     * @return string
      */
-    public function getISOFormat()
+    public function getISOFormat(): string
     {
         return DBDatetime::ISO_DATETIME;
     }
diff --git a/src/ORM/FieldType/DBDecimal.php b/src/ORM/FieldType/DBDecimal.php
index dcf2d7e8046..0b337786e34 100644
--- a/src/ORM/FieldType/DBDecimal.php
+++ b/src/ORM/FieldType/DBDecimal.php
@@ -2,71 +2,55 @@
 
 namespace SilverStripe\ORM\FieldType;
 
+use SilverStripe\Forms\FormField;
 use SilverStripe\Forms\NumericField;
 use SilverStripe\ORM\DB;
+use SilverStripe\View\ViewableData;
 
 /**
  * Represents a Decimal field.
  */
 class DBDecimal extends DBField
 {
-
     /**
      * Whole number size
-     *
-     * @var int
      */
-    protected $wholeSize = 9;
+    protected int $wholeSize = 9;
 
     /**
      * Decimal scale
-     *
-     * @var int
      */
-    protected $decimalSize = 2;
+    protected int $decimalSize = 2;
 
     /**
      * Default value
-     *
-     * @var string
      */
-    protected $defaultValue = 0;
+    protected float|int|string $defaultValue = 0;
 
     /**
      * Create a new Decimal field.
-     *
-     * @param string $name
-     * @param int $wholeSize
-     * @param int $decimalSize
-     * @param float|int $defaultValue
      */
-    public function __construct($name = null, $wholeSize = 9, $decimalSize = 2, $defaultValue = 0)
+    public function __construct(?string $name = null, ?int $wholeSize = 9, ?int $decimalSize = 2, float|int $defaultValue = 0)
     {
         $this->wholeSize = is_int($wholeSize) ? $wholeSize : 9;
         $this->decimalSize = is_int($decimalSize) ? $decimalSize : 2;
 
-        $this->defaultValue = number_format((float) $defaultValue, $decimalSize ?? 0);
+        $this->defaultValue = number_format((float) $defaultValue, $this->decimalSize);
 
         parent::__construct($name);
     }
 
-    /**
-     * @return float
-     */
-    public function Nice()
+    public function Nice(): string
     {
         return number_format($this->value ?? 0.0, $this->decimalSize ?? 0);
     }
 
-    /**
-     * @return int
-     */
-    public function Int()
+    public function Int(): int
     {
         return floor($this->value ?? 0.0);
     }
 
-    public function requireField()
+    public function requireField(): void
     {
         $parts = [
             'datatype' => 'decimal',
@@ -83,16 +67,16 @@ public function requireField()
         DB::require_field($this->tableName, $this->name, $values);
     }
 
-    public function saveInto($dataObject)
+    public function saveInto(ViewableData $model): void
     {
         $fieldName = $this->name;
 
         if ($fieldName) {
             if ($this->value instanceof DBField) {
-                $this->value->saveInto($dataObject);
+                $this->value->saveInto($model);
             } else {
                 $value = (float) preg_replace('/[^0-9.\-\+]/', '', $this->value ?? '');
-                $dataObject->__set($fieldName, $value);
+                $model->__set($fieldName, $value);
             }
         } else {
             throw new \UnexpectedValueException(
@@ -101,27 +85,18 @@ public function saveInto($dataObject)
         }
     }
 
-    /**
-     * @param string $title
-     * @param array $params
-     *
-     * @return NumericField
-     */
-    public function scaffoldFormField($title = null, $params = null)
+    public function scaffoldFormField(?string $title = null, array $params = []): ?FormField
     {
         return NumericField::create($this->name, $title)
             ->setScale($this->decimalSize);
     }
 
-    /**
-     * @return float
-     */
-    public function nullValue()
+    public function nullValue(): ?int
     {
         return 0;
     }
 
-    public function prepValueForDB($value)
+    public function prepValueForDB(mixed $value): float|int|null
     {
         if ($value === true) {
             return 1;
diff --git a/src/ORM/FieldType/DBDouble.php b/src/ORM/FieldType/DBDouble.php
index 04b91eab5ae..17d7f69eeb4 100644
--- a/src/ORM/FieldType/DBDouble.php
+++ b/src/ORM/FieldType/DBDouble.php
@@ -11,7 +11,7 @@
 class DBDouble extends DBFloat
 {
 
-    public function requireField()
+    public function requireField(): void
     {
         // HACK: MSSQL does not support double so we're using float instead
         if (DB::get_conn() instanceof MySQLDatabase) {
diff --git a/src/ORM/FieldType/DBEnum.php b/src/ORM/FieldType/DBEnum.php
index b85dec01361..7f0eaba527a 100644
--- a/src/ORM/FieldType/DBEnum.php
+++ b/src/ORM/FieldType/DBEnum.php
@@ -4,6 +4,8 @@
 
 use SilverStripe\Core\Config\Config;
 use SilverStripe\Forms\DropdownField;
+use SilverStripe\Forms\FormField;
+use SilverStripe\Forms\SelectField;
 use SilverStripe\ORM\ArrayLib;
 use SilverStripe\ORM\Connect\MySQLDatabase;
 use SilverStripe\ORM\DB;
@@ -15,35 +17,28 @@
  */
 class DBEnum extends DBString
 {
-
     /**
      * List of enum values
-     *
-     * @var array
      */
-    protected $enum = [];
+    protected array $enum = [];
 
     /**
      * Default value
-     *
-     * @var string|null
      */
-    protected $default = null;
+    protected ?string $default = null;
 
-    private static $default_search_filter_class = 'ExactMatchFilter';
+    private static string $default_search_filter_class = 'ExactMatchFilter';
 
     /**
      * Internal cache for obsolete enum values. The top level keys are the table, each of which contains
      * nested arrays with keys mapped to field names. The values of the lowest level array are the enum values
-     *
-     * @var array
      */
-    protected static $enum_cache = [];
+    protected static array $enum_cache = [];
 
     /**
      * Clear all cached enum values.
      */
-    public static function flushCache()
+    public static function flushCache(): void
     {
         DBEnum::$enum_cache = [];
     }
@@ -60,15 +55,18 @@ public static function flushCache()
      *  "MyField" => "Enum(['Val1', 'Val2', 'Val3'], 'Val1')" // Supports array notation as well
      * </code>
      *
-     * @param string $name
      * @param string|array $enum A string containing a comma separated list of options or an array of Vals.
      * @param string|int|null $default The default option, which is either NULL or one of the items in the enumeration.
      * If passing in an integer (non-string) it will default to the index of that item in the list.
      * Set to null or empty string to allow empty values
-     * @param array  $options Optional parameters for this DB field
+     * @param array $options Optional parameters for this DB field
      */
-    public function __construct($name = null, $enum = null, $default = 0, $options = [])
-    {
+    public function __construct(
+        ?string $name = null,
+        string|array|null $enum = null,
+        string|int|null $default = 0,
+        array $options = []
+    ) {
         if ($enum) {
             $this->setEnum($enum);
             $enum = $this->getEnum();
@@ -94,10 +92,7 @@ public function __construct($name = null, $enum = null, $default = 0, $options =
         parent::__construct($name, $options);
     }
 
-    /**
-     * @return void
-     */
-    public function requireField()
+    public function requireField(): void
     {
         $charset = Config::inst()->get(MySQLDatabase::class, 'charset');
         $collation = Config::inst()->get(MySQLDatabase::class, 'collation');
@@ -121,18 +116,15 @@ public function requireField()
     }
 
     /**
-     * Return a dropdown field suitable for editing this field.
-     *
-     * @param string $title
-     * @param string $name
-     * @param bool $hasEmpty
-     * @param string $value
-     * @param string $emptyString
-     * @return DropdownField
+     * Return a form field suitable for editing this field.
      */
-    public function formField($title = null, $name = null, $hasEmpty = false, $value = '', $emptyString = null)
-    {
-
+    public function formField(
+        ?string $title = null,
+        ?string $name = null,
+        bool $hasEmpty = false,
+        ?string $value = '',
+        ?string $emptyString = null
+    ): SelectField {
         if (!$title) {
             $title = $this->getName();
         }
@@ -148,16 +140,12 @@ public function formField($title = null, $name = null, $hasEmpty = false, $value
         return $field;
     }
 
-    public function scaffoldFormField($title = null, $params = null)
+    public function scaffoldFormField(?string $title = null, array $params = []): ?FormField
     {
         return $this->formField($title);
     }
 
-    /**
-     * @param string $title
-     * @return DropdownField
-     */
-    public function scaffoldSearchField($title = null)
+    public function scaffoldSearchField(?string $title = null): ?FormField
     {
         $anyText = _t(__CLASS__ . '.ANY', 'Any');
         return $this->formField($title, null, true, '', "($anyText)");
@@ -166,12 +154,8 @@ public function scaffoldSearchField($title = null)
     /**
      * Returns the values of this enum as an array, suitable for insertion into
      * a {@link DropdownField}
-     *
-     * @param bool $hasEmpty
-     *
-     * @return array
      */
-    public function enumValues($hasEmpty = false)
+    public function enumValues(bool $hasEmpty = false): array
     {
         return ($hasEmpty)
             ? array_merge(['' => ''], ArrayLib::valuekey($this->getEnum()))
@@ -180,15 +164,12 @@ public function enumValues($hasEmpty = false)
 
     /**
      * Get list of enum values
-     *
-     * @return array
      */
-    public function getEnum()
+    public function getEnum(): array
     {
         return $this->enum;
     }
 
-
     /**
      * Get the list of enum values, including obsolete values still present in the database
      *
@@ -196,10 +177,8 @@ public function getEnum()
      * then only known enum values are returned.
      *
      * Values cached in this method can be cleared via `DBEnum::flushCache();`
-     *
-     * @return array
      */
-    public function getEnumObsolete()
+    public function getEnumObsolete(): array
     {
         // Without a table or field specified, we can only retrieve known enum values
         $table = $this->getTable();
@@ -232,11 +211,8 @@ public function getEnumObsolete()
 
     /**
      * Set enum options
-     *
-     * @param string|array $enum
-     * @return $this
      */
-    public function setEnum($enum)
+    public function setEnum(string|array $enum): static
     {
         if (!is_array($enum)) {
             $enum = preg_split(
@@ -250,22 +226,17 @@ public function setEnum($enum)
     }
 
     /**
-     * Get default vwalue
-     *
-     * @return string|null
+     * Get default value
      */
-    public function getDefault()
+    public function getDefault(): ?string
     {
         return $this->default;
     }
 
     /**
      * Set default value
-     *
-     * @param string $default
-     * @return $this
      */
-    public function setDefault($default)
+    public function setDefault(?string $default): static
     {
         $this->default = $default;
         $this->setDefaultValue($default);
diff --git a/src/ORM/FieldType/DBField.php b/src/ORM/FieldType/DBField.php
index 94b5ee31f15..0053057e4ef 100644
--- a/src/ORM/FieldType/DBField.php
+++ b/src/ORM/FieldType/DBField.php
@@ -7,7 +7,6 @@
 use SilverStripe\Core\Injector\Injector;
 use SilverStripe\Forms\FormField;
 use SilverStripe\Forms\TextField;
-use SilverStripe\ORM\DataObject;
 use SilverStripe\ORM\Filters\SearchFilter;
 use SilverStripe\ORM\Queries\SQLSelect;
 use SilverStripe\View\ViewableData;
@@ -36,7 +35,7 @@
  *
  * <code>
  * class Blob extends DBField {
- *  function requireField() {
+ *  function requireField(): void {
  *      DB::require_field($this->tableName, $this->name, "blob");
  *  }
  * }
@@ -47,65 +46,47 @@ abstract class DBField extends ViewableData implements DBIndexable
 
     /**
      * Raw value of this field
-     *
-     * @var mixed
      */
-    protected $value;
+    protected mixed $value = null;
 
     /**
      * Table this field belongs to
-     *
-     * @var string
      */
-    protected $tableName;
+    protected ?string $tableName = null;
 
     /**
      * Name of this field
-     *
-     * @var string
      */
-    protected $name;
+    protected ?string $name = null;
 
     /**
      * Used for generating DB schema. {@see DBSchemaManager}
-     *
-     * @var array
+     * Despite its name, this seems to be a string
      */
     protected $arrayValue;
 
     /**
      * Optional parameters for this field
-     *
-     * @var array
      */
-    protected $options = [];
+    protected array $options = [];
 
     /**
      * The escape type for this field when inserted into a template - either "xml" or "raw".
-     *
-     * @var string
-     * @config
      */
-    private static $escape_type = 'raw';
+    private static string $escape_type = 'raw';
 
     /**
      * Subclass of {@link SearchFilter} for usage in {@link defaultSearchFilter()}.
-     *
-     * @var string
-     * @config
      */
-    private static $default_search_filter_class = 'PartialMatchFilter';
+    private static string $default_search_filter_class = 'PartialMatchFilter';
 
     /**
      * The type of index to use for this field. Can either be a string (one of the DBIndexable type options) or a
      * boolean. When a boolean is given, false will not index the field, and true will use the default index type.
-     *
-     * @var string|bool
-     * @config
      */
-    private static $index = false;
+    private static string|bool $index = false;
 
-    private static $casting = [
+    private static array $casting = [
         'ATT' => 'HTMLFragment',
         'CDATA' => 'HTMLFragment',
         'HTML' => 'HTMLFragment',
@@ -119,20 +100,18 @@ abstract class DBField extends ViewableData implements DBIndexable
     ];
 
     /**
-     * @var $default mixed Default-value in the database.
+     * Default value in the database.
      * Might be overridden on DataObject-level, but still useful for setting defaults on
      * already existing records after a db-build.
      */
-    protected $defaultVal;
+    protected mixed $defaultVal = null;
 
     /**
      * Provide the DBField name and an array of options, e.g. ['index' => true], or ['nullifyEmpty' => false]
      *
-     * @param  string $name
-     * @param  array  $options
      * @throws InvalidArgumentException If $options was passed by not an array
      */
-    public function __construct($name = null, $options = [])
+    public function __construct(?string $name = null, array $options = [])
     {
         $this->name = $name;
 
@@ -154,12 +133,11 @@ public function __construct($name = null, $options = [])
      * @param string $spec Class specification to construct. May include both service name and additional
      * constructor arguments in the same format as DataObject.db config.
      * @param mixed $value value of field
-     * @param string $name Name of field
+     * @param null|string $name Name of field
      * @param mixed $args Additional arguments to pass to constructor if not using args in service $spec
      * Note: Will raise a warning if using both
-     * @return static
      */
-    public static function create_field($spec, $value, $name = null, ...$args)
+    public static function create_field(string $spec, mixed $value, ?string $name = null, mixed ...$args): static
     {
         // Raise warning if inconsistent with DataObject::dbObject() behaviour
         // This will cause spec args to be shifted down by the number of provided $args
@@ -182,12 +160,8 @@ public static function create_field($spec, $value, $name = null, ...$args)
      * the first place you can set a name.
      *
      * If you try an alter the name a warning will be thrown.
-     *
-     * @param string $name
-     *
-     * @return $this
      */
-    public function setName($name)
+    public function setName(?string $name): static
     {
         if ($this->name && $this->name !== $name) {
             user_error("DBField::setName() shouldn't be called once a DBField already has a name."
@@ -201,20 +175,16 @@ public function setName($name)
 
     /**
      * Returns the name of this field.
-     *
-     * @return string
      */
-    public function getName()
+    public function getName(): string
     {
-        return $this->name;
+        return $this->name ?? '';
     }
 
     /**
      * Returns the value of this field.
-     *
-     * @return mixed
      */
-    public function getValue()
+    public function getValue(): mixed
     {
         return $this->value;
     }
@@ -228,14 +198,12 @@ public function getValue()
      * and actually changing its values, it needs a {@link $markChanged}
      * parameter.
      *
-     * @param mixed $value
-     * @param DataObject|array $record An array or object that this field is part of
+     * @param null|ViewableData|array $record An array or object that this field is part of
      * @param bool $markChanged Indicate whether this field should be marked changed.
      *  Set to FALSE if you are initializing this field after construction, rather
      *  than setting a new value.
-     * @return $this
      */
-    public function setValue($value, $record = null, $markChanged = true)
+    public function setValue(mixed $value, null|array|ViewableData $record = null, bool $markChanged = true): static
     {
         $this->value = $value;
         return $this;
@@ -243,21 +211,16 @@ public function setValue($value, $record = null, $markChanged = true)
 
     /**
      * Get default value assigned at the DB level
-     *
-     * @return mixed
      */
-    public function getDefaultValue()
+    public function getDefaultValue(): mixed
     {
         return $this->defaultVal;
     }
 
     /**
      * Set default value to use at the DB level
-     *
-     * @param mixed $defaultValue
-     * @return $this
      */
-    public function setDefaultValue($defaultValue)
+    public function setDefaultValue(mixed $defaultValue): static
     {
         $this->defaultVal = $defaultValue;
         return $this;
@@ -265,11 +228,8 @@ public function setDefaultValue($defaultValue)
 
     /**
      * Update the optional parameters for this field
-     *
-     * @param array $options Array of options
-     * @return $this
      */
-    public function setOptions(array $options = [])
+    public function setOptions(array $options = []): static
     {
         $this->options = $options;
         return $this;
@@ -277,15 +237,13 @@ public function setOptions(array $options = [])
 
     /**
      * Get optional parameters for this field
-     *
-     * @return array
      */
-    public function getOptions()
+    public function getOptions(): array
     {
         return $this->options;
     }
 
-    public function setIndexType($type)
+    public function setIndexType($type): string|bool
     {
         if (!is_bool($type)
             && !in_array($type, [DBIndexable::TYPE_INDEX, DBIndexable::TYPE_UNIQUE, DBIndexable::TYPE_FULLTEXT])
@@ -320,10 +278,8 @@ public function getIndexType()
     /**
      * Determines if the field has a value which is not considered to be 'null'
      * in a database context.
-     *
-     * @return boolean
      */
-    public function exists()
+    public function exists(): bool
     {
         return (bool)$this->value;
     }
@@ -336,7 +292,7 @@ public function exists()
      * @param mixed $value The value to check
      * @return mixed The raw value, or escaped parameterised details
      */
-    public function prepValueForDB($value)
+    public function prepValueForDB(mixed $value): mixed
     {
         if ($value === null ||
             $value === "" ||
@@ -358,10 +314,8 @@ public function prepValueForDB($value)
      * can also be used to apply special SQL-commands
      * to the raw value (e.g. for GIS functionality).
      * {@see prepValueForDB}
-     *
-     * @param array $manipulation
      */
-    public function writeToManipulation(&$manipulation)
+    public function writeToManipulation(array &$manipulation): void
     {
         $manipulation['fields'][$this->name] = $this->exists()
             ? $this->prepValueForDB($this->value) : $this->nullValue();
@@ -375,20 +329,15 @@ public function writeToManipulation(&$manipulation)
      * SELECT <tablename>.* which
      * gets you the default representations
      * of all columns.
-     *
-     * @param SQLSelect $query
      */
-    public function addToQuery(&$query)
+    public function addToQuery(SQLSelect &$query)
     {
     }
 
     /**
      * Assign this DBField to a table
-     *
-     * @param string $tableName
-     * @return $this
      */
-    public function setTable($tableName)
+    public function setTable(string $tableName): static
     {
         $this->tableName = $tableName;
         return $this;
@@ -396,20 +345,16 @@ public function setTable($tableName)
 
     /**
      * Get the table this field belongs to, if assigned
-     *
-     * @return string|null
      */
-    public function getTable()
+    public function getTable(): ?string
     {
         return $this->tableName;
     }
 
     /**
      * Determine 'default' casting for this field.
-     *
-     * @return string
      */
-    public function forTemplate()
+    public function forTemplate(): string
     {
         // Default to XML encoding
         return $this->XML();
@@ -417,40 +362,32 @@ public function forTemplate()
 
     /**
      * Gets the value appropriate for a HTML attribute string
-     *
-     * @return string
      */
-    public function HTMLATT()
+    public function HTMLATT(): string
     {
         return Convert::raw2htmlatt($this->RAW());
     }
 
     /**
      * urlencode this string
-     *
-     * @return string
      */
-    public function URLATT()
+    public function URLATT(): string
     {
         return urlencode($this->RAW() ?? '');
     }
 
     /**
      * rawurlencode this string
-     *
-     * @return string
      */
-    public function RAWURLATT()
+    public function RAWURLATT(): string
     {
         return rawurlencode($this->RAW() ?? '');
     }
 
     /**
      * Gets the value appropriate for a HTML attribute string
-     *
-     * @return string
      */
-    public function ATT()
+    public function ATT(): string
     {
         return Convert::raw2att($this->RAW());
     }
@@ -458,60 +395,48 @@ public function ATT()
     /**
      * Gets the raw value for this field.
      * Note: Skips processors implemented via forTemplate()
-     *
-     * @return mixed
      */
-    public function RAW()
+    public function RAW(): mixed
     {
         return $this->getValue();
     }
 
     /**
      * Gets javascript string literal value
-     *
-     * @return string
      */
-    public function JS()
+    public function JS(): string
     {
         return Convert::raw2js($this->RAW());
     }
 
     /**
      * Return JSON encoded value
-     *
-     * @return string
      */
-    public function JSON()
+    public function JSON(): string
     {
         return json_encode($this->RAW());
     }
 
     /**
      * Alias for {@see XML()}
-     *
-     * @return string
      */
-    public function HTML()
+    public function HTML(): string
     {
         return $this->XML();
     }
 
     /**
      * XML encode this value
-     *
-     * @return string
      */
-    public function XML()
+    public function XML(): string
     {
         return Convert::raw2xml($this->RAW());
     }
 
     /**
      * Safely escape for XML string
-     *
-     * @return string
      */
-    public function CDATA()
+    public function CDATA(): string
     {
         return $this->XML();
     }
@@ -519,20 +444,16 @@ public function CDATA()
     /**
      * Returns the value to be set in the database to blank this field.
      * Usually it's a choice between null, 0, and ''
-     *
-     * @return mixed
      */
-    public function nullValue()
+    public function nullValue(): mixed
     {
         return null;
     }
 
     /**
      * Saves this field to the given data object.
-     *
-     * @param DataObject $dataObject
      */
-    public function saveInto($dataObject)
+    public function saveInto(ViewableData $model): void
     {
         $fieldName = $this->name;
         if (empty($fieldName)) {
@@ -541,9 +462,9 @@ public function saveInto($dataObject)
             );
         }
         if ($this->value instanceof DBField) {
-            $this->value->saveInto($dataObject);
+            $this->value->saveInto($model);
         } else {
-            $dataObject->__set($fieldName, $this->value);
+            $model->__set($fieldName, $this->value);
         }
     }
 
@@ -554,10 +475,8 @@ public function saveInto($dataObject)
      * Used by {@link SearchContext}, {@link ModelAdmin}, {@link DataObject::scaffoldFormFields()}
      *
      * @param string $title Optional. Localized title of the generated instance
-     * @param array $params
-     * @return FormField
      */
-    public function scaffoldFormField($title = null, $params = null)
+    public function scaffoldFormField(?string $title = null, array $params = []): ?FormField
     {
         return TextField::create($this->name, $title);
     }
@@ -569,30 +488,28 @@ public function scaffoldFormField($title = null, $params = null)
      * Used by {@link SearchContext}, {@link ModelAdmin}, {@link DataObject::scaffoldFormFields()}.
      *
      * @param string $title Optional. Localized title of the generated instance
-     * @return FormField
      */
-    public function scaffoldSearchField($title = null)
+    public function scaffoldSearchField(?string $title = null): ?FormField
     {
         return $this->scaffoldFormField($title);
     }
 
     /**
      * @param string $name Override name of this field
-     * @return SearchFilter
      */
-    public function defaultSearchFilter($name = null)
+    public function defaultSearchFilter(?string $name = null): SearchFilter
     {
         $name = ($name) ? $name : $this->name;
-        $filterClass = $this->config()->get('default_search_filter_class');
+        $filterClass = static::config()->get('default_search_filter_class');
         return Injector::inst()->create($filterClass, $name);
     }
 
     /**
      * Add the field to the underlying database.
      */
-    abstract public function requireField();
+    abstract public function requireField(): void;
 
-    public function debug()
+    public function debug(): string
     {
         return <<<DBG
 <ul>
@@ -603,40 +520,31 @@ public function debug()
 DBG;
     }
 
-    public function __toString()
+    public function __toString(): string
     {
         return (string)$this->forTemplate();
     }
 
-    /**
-     * @return array
-     */
     public function getArrayValue()
     {
         return $this->arrayValue;
     }
 
-    /**
-     * @param array $value
-     * @return $this
-     */
-    public function setArrayValue($value)
+    public function setArrayValue($value): static
     {
         $this->arrayValue = $value;
         return $this;
     }
 
     /**
-     * Get formfield schema value
-     *
-     * @return string|array Encoded string for use in formschema response
+     * Get formfield schema value for use in formschema response
      */
-    public function getSchemaValue()
+    public function getSchemaValue(): mixed
     {
         return $this->RAW();
     }
 
-    public function getIndexSpecs()
+    public function getIndexSpecs(): ?array
     {
         $type = $this->getIndexType();
         if ($type) {
@@ -652,9 +560,8 @@ public function getIndexSpecs()
      * Whether or not this DBField only accepts scalar values.
      *
      * Composite DBFields can override this method and return `false` so they can accept arrays of values.
-     * @return boolean
      */
-    public function scalarValueOnly()
+    public function scalarValueOnly(): bool
     {
         return true;
     }
diff --git a/src/ORM/FieldType/DBFloat.php b/src/ORM/FieldType/DBFloat.php
index 8fd0e129eef..0bec2866db7 100644
--- a/src/ORM/FieldType/DBFloat.php
+++ b/src/ORM/FieldType/DBFloat.php
@@ -2,6 +2,7 @@
 
 namespace SilverStripe\ORM\FieldType;
 
+use SilverStripe\Forms\FormField;
 use SilverStripe\Forms\NumericField;
 use SilverStripe\ORM\DB;
 
@@ -10,15 +11,14 @@
  */
 class DBFloat extends DBField
 {
-
-    public function __construct($name = null, $defaultVal = 0)
+    public function __construct(?string $name = null, float|int $defaultVal = 0)
     {
         $this->defaultVal = is_float($defaultVal) ? $defaultVal : (float) 0;
 
         parent::__construct($name);
     }
 
-    public function requireField()
+    public function requireField(): void
     {
         $parts = [
             'datatype' => 'float',
@@ -35,34 +35,34 @@ public function requireField()
      *
      * @uses number_format()
      */
-    public function Nice()
+    public function Nice(): string
     {
         return number_format($this->value ?? 0.0, 2);
     }
 
-    public function Round($precision = 3)
+    public function Round($precision = 3): float
     {
         return round($this->value ?? 0.0, $precision ?? 0);
     }
 
-    public function NiceRound($precision = 3)
+    public function NiceRound($precision = 3): string
     {
         return number_format(round($this->value ?? 0.0, $precision ?? 0), $precision ?? 0);
     }
 
-    public function scaffoldFormField($title = null, $params = null)
+    public function scaffoldFormField(?string $title = null, array $params = []): ?FormField
     {
         $field = NumericField::create($this->name, $title);
         $field->setScale(null); // remove no-decimal restriction
         return $field;
     }
 
-    public function nullValue()
+    public function nullValue(): ?int
     {
         return 0;
     }
 
-    public function prepValueForDB($value)
+    public function prepValueForDB(mixed $value): float|int|null
     {
         if ($value === true) {
             return 1;
diff --git a/src/ORM/FieldType/DBForeignKey.php b/src/ORM/FieldType/DBForeignKey.php
index 4265491ae18..5f7d95f55da 100644
--- a/src/ORM/FieldType/DBForeignKey.php
+++ b/src/ORM/FieldType/DBForeignKey.php
@@ -6,9 +6,11 @@
 use SilverStripe\Assets\Image;
 use SilverStripe\Core\Injector\Injector;
 use SilverStripe\Forms\FileHandleField;
+use SilverStripe\Forms\FormField;
 use SilverStripe\Forms\SearchableDropdownField;
 use SilverStripe\ORM\DataList;
 use SilverStripe\ORM\DataObject;
+use SilverStripe\View\ViewableData;
 
 /**
  * A special type Int field used for foreign keys in has_one relationships.
@@ -22,40 +24,26 @@
  */
 class DBForeignKey extends DBInt
 {
-    /**
-     * @var DataObject
-     */
-    protected $object;
+    protected ?DataObject $object;
 
     /**
      * Number of related objects to show in a scaffolded searchable dropdown field before it
      * switches to using lazyloading.
      * This will also be used as the lazy load limit
-     *
-     * @config
-     * @var int
      */
-    private static $dropdown_field_threshold = 100;
-
-    private static $index = true;
+    private static int $dropdown_field_threshold = 100;
 
-    private static $default_search_filter_class = 'ExactMatchFilter';
+    private static string|bool $index = true;
 
-    /**
-     * Cache for multiple subsequent calls to scaffold form fields with the same foreign key object
-     *
-     * @var array
-     * @deprecated 5.2.0 Will be removed without equivalent functionality to replace it
-     */
-    protected static $foreignListCache = [];
+    private static string $default_search_filter_class = 'ExactMatchFilter';
 
-    public function __construct($name, $object = null)
+    public function __construct(?string $name, ?DataObject $object = null)
     {
         $this->object = $object;
         parent::__construct($name);
     }
 
-    public function scaffoldFormField($title = null, $params = null)
+    public function scaffoldFormField(?string $title = null, array $params = []): ?FormField
     {
         if (empty($this->object)) {
             return null;
@@ -70,11 +58,11 @@ public function scaffoldFormField($title = null, $params = null)
         return $field;
     }
 
-    public function setValue($value, $record = null, $markChanged = true)
+    public function setValue(mixed $value, null|array|ViewableData $record = null, bool $markChanged = true): static
     {
         if ($record instanceof DataObject) {
             $this->object = $record;
         }
-        parent::setValue($value, $record, $markChanged);
+        return parent::setValue($value, $record, $markChanged);
     }
 }
diff --git a/src/ORM/FieldType/DBHTMLText.php b/src/ORM/FieldType/DBHTMLText.php
index 4241c94d59c..6ee57d2c998 100644
--- a/src/ORM/FieldType/DBHTMLText.php
+++ b/src/ORM/FieldType/DBHTMLText.php
@@ -4,6 +4,7 @@
 
 use SilverStripe\Control\HTTP;
 use SilverStripe\Core\Convert;
+use SilverStripe\Forms\FormField;
 use SilverStripe\Forms\HTMLEditor\HTMLEditorField;
 use SilverStripe\Forms\TextField;
 use SilverStripe\View\Parsers\HTMLValue;
@@ -25,9 +26,9 @@
  */
 class DBHTMLText extends DBText
 {
-    private static $escape_type = 'xml';
+    private static string $escape_type = 'xml';
 
-    private static $casting = [
+    private static array $casting = [
         "AbsoluteLinks" => "HTMLFragment",
         // DBString conversion / summary methods
         // Not overridden, but returns HTML instead of plain text.
@@ -37,68 +38,52 @@ class DBHTMLText extends DBText
 
     /**
      * Enable shortcode parsing on this field
-     *
-     * @var bool
      */
-    protected $processShortcodes = false;
+    protected bool $processShortcodes = false;
+
+    /**
+     * List of html properties to whitelist
+     */
+    protected array $whitelist = [];
 
     /**
      * Check if shortcodes are enabled
-     *
-     * @return bool
      */
-    public function getProcessShortcodes()
+    public function getProcessShortcodes(): bool
     {
         return $this->processShortcodes;
     }
 
     /**
      * Set shortcodes on or off by default
-     *
-     * @param bool $process
-     * @return $this
      */
-    public function setProcessShortcodes($process)
+    public function setProcessShortcodes(bool $process): static
     {
-        $this->processShortcodes = (bool)$process;
+        $this->processShortcodes = $process;
         return $this;
     }
 
     /**
      * List of html properties to whitelist
-     *
-     * @var array
-     */
-    protected $whitelist = [];
-
-    /**
-     * List of html properties to whitelist
-     *
-     * @return array
      */
-    public function getWhitelist()
+    public function getWhitelist(): array
     {
         return $this->whitelist;
     }
 
     /**
      * Set list of html properties to whitelist
-     *
-     * @param array $whitelist
-     * @return $this
      */
-    public function setWhitelist($whitelist)
+    public function setWhitelist(string|array $whitelist): static
     {
         if (!is_array($whitelist)) {
-            $whitelist = preg_split('/\s*,\s*/', $whitelist ?? '');
+            $whitelist = preg_split('/\s*,\s*/', $whitelist);
         }
         $this->whitelist = $whitelist;
         return $this;
     }
 
     /**
-     * @param array $options
-     *
      * Options accepted in addition to those provided by Text:
      *
      *   - shortcodes: If true, shortcodes will be turned into the appropriate HTML.
@@ -110,10 +95,8 @@ public function setWhitelist($whitelist)
      *                Text nodes outside of HTML tags are filtered out by default, but may be included by adding
      *                the text() directive. E.g. 'link,meta,text()' will allow only <link /> <meta /> and text at
      *                the root level.
-     *
-     * @return $this
      */
-    public function setOptions(array $options = [])
+    public function setOptions(array $options = []): static
     {
         if (array_key_exists("shortcodes", $options ?? [])) {
             $this->setProcessShortcodes(!!$options["shortcodes"]);
@@ -126,7 +109,7 @@ public function setOptions(array $options = [])
         return parent::setOptions($options);
     }
 
-    public function RAW()
+    public function RAW(): ?string
     {
         if ($this->processShortcodes) {
             return ShortcodeParser::get_active()->parse($this->value);
@@ -136,25 +119,22 @@ public function RAW()
 
     /**
      * Return the value of the field with relative links converted to absolute urls (with placeholders parsed).
-     * @return string
      */
-    public function AbsoluteLinks()
+    public function AbsoluteLinks(): string
     {
         return HTTP::absoluteURLs($this->forTemplate());
     }
 
-    public function forTemplate()
+    public function forTemplate(): string
     {
         // Suppress XML encoding for DBHtmlText
-        return $this->RAW();
+        return $this->RAW() ?? '';
     }
 
     /**
      * Safely escape for XML string
-     *
-     * @return string
      */
-    public function CDATA()
+    public function CDATA(): string
     {
         return sprintf(
             '<![CDATA[%s]]>',
@@ -162,7 +142,7 @@ public function CDATA()
         );
     }
 
-    public function prepValueForDB($value)
+    public function prepValueForDB(mixed $value): ?string
     {
         return parent::prepValueForDB($this->whitelistContent($value));
     }
@@ -173,7 +153,7 @@ public function prepValueForDB($value)
      * @param string $value Input html content
      * @return string Value with all non-whitelisted content stripped (if applicable)
      */
-    public function whitelistContent($value)
+    public function whitelistContent(string $value): string
     {
         if ($this->whitelist) {
             $dom = HTMLValue::create($value);
@@ -199,22 +179,20 @@ public function whitelistContent($value)
         return $value;
     }
 
-    public function scaffoldFormField($title = null, $params = null)
+    public function scaffoldFormField(?string $title = null, array $params = []): ?FormField
     {
         return HTMLEditorField::create($this->name, $title);
     }
 
-    public function scaffoldSearchField($title = null)
+    public function scaffoldSearchField(?string $title = null): ?FormField
     {
         return new TextField($this->name, $title);
     }
 
     /**
      * Get plain-text version
-     *
-     * @return string
      */
-    public function Plain()
+    public function Plain(): string
     {
         // Preserve line breaks
         $text = preg_replace('/\<br(\s*)?\/?\>/i', "\n", $this->RAW() ?? '');
@@ -232,7 +210,7 @@ public function Plain()
         return trim(Convert::xml2raw($text) ?? '');
     }
 
-    public function getSchemaValue()
+    public function getSchemaValue(): ?array
     {
         // Form schema format as HTML
         $value = $this->RAW();
@@ -242,7 +220,7 @@ public function getSchemaValue()
         return null;
     }
 
-    public function exists()
+    public function exists(): bool
     {
         // Optimisation: don't process shortcode just for ->exists()
         $value = $this->getValue();
diff --git a/src/ORM/FieldType/DBHTMLVarchar.php b/src/ORM/FieldType/DBHTMLVarchar.php
index 3cc0f083743..f11762eaa8d 100644
--- a/src/ORM/FieldType/DBHTMLVarchar.php
+++ b/src/ORM/FieldType/DBHTMLVarchar.php
@@ -3,6 +3,7 @@
 namespace SilverStripe\ORM\FieldType;
 
 use SilverStripe\Core\Convert;
+use SilverStripe\Forms\FormField;
 use SilverStripe\Forms\HTMLEditor\HTMLEditorField;
 use SilverStripe\Forms\TextField;
 use SilverStripe\View\Parsers\ShortcodeParser;
@@ -14,10 +15,9 @@
  */
 class DBHTMLVarchar extends DBVarchar
 {
+    private static string $escape_type = 'xml';
 
-    private static $escape_type = 'xml';
-
-    private static $casting = [
+    private static array $casting = [
         // DBString conversion / summary methods
         // Not overridden, but returns HTML instead of plain text.
         "LowerCase" => "HTMLFragment",
@@ -26,35 +26,27 @@ class DBHTMLVarchar extends DBVarchar
 
     /**
      * Enable shortcode parsing on this field
-     *
-     * @var bool
      */
-    protected $processShortcodes = false;
+    protected bool $processShortcodes = false;
 
     /**
      * Check if shortcodes are enabled
-     *
-     * @return bool
      */
-    public function getProcessShortcodes()
+    public function getProcessShortcodes(): bool
     {
         return $this->processShortcodes;
     }
 
     /**
      * Set shortcodes on or off by default
-     *
-     * @param bool $process
-     * @return $this
      */
-    public function setProcessShortcodes($process)
+    public function setProcessShortcodes(bool $process): static
     {
-        $this->processShortcodes = (bool)$process;
+        $this->processShortcodes = $process;
         return $this;
     }
+
     /**
-     * @param array $options
-     *
      * Options accepted in addition to those provided by Text:
      *
      *   - shortcodes: If true, shortcodes will be turned into the appropriate HTML.
@@ -66,10 +58,8 @@ public function setProcessShortcodes($process)
      *                Text nodes outside of HTML tags are filtered out by default, but may be included by adding
      *                the text() directive. E.g. 'link,meta,text()' will allow only <link /> <meta /> and text at
      *                the root level.
-     *
-     * @return $this
      */
-    public function setOptions(array $options = [])
+    public function setOptions(array $options = []): static
     {
         if (array_key_exists("shortcodes", $options ?? [])) {
             $this->setProcessShortcodes(!!$options["shortcodes"]);
@@ -78,13 +68,13 @@ public function setOptions(array $options = [])
         return parent::setOptions($options);
     }
 
-    public function forTemplate()
+    public function forTemplate(): string
     {
         // Suppress XML encoding for DBHtmlText
-        return $this->RAW();
+        return $this->RAW() ?? '';
     }
 
-    public function RAW()
+    public function RAW(): ?string
     {
         if ($this->processShortcodes) {
             return ShortcodeParser::get_active()->parse($this->value);
@@ -94,10 +84,8 @@ public function RAW()
 
     /**
      * Safely escape for XML string
-     *
-     * @return string
      */
-    public function CDATA()
+    public function CDATA(): string
     {
         return sprintf(
             '<![CDATA[%s]]>',
@@ -109,10 +97,8 @@ public function CDATA()
      * Get plain-text version.
      *
      * Note: unlike DBHTMLText, this doesn't respect line breaks / paragraphs
-     *
-     * @return string
      */
-    public function Plain()
+    public function Plain(): string
     {
         // Strip out HTML
         $text = strip_tags($this->RAW() ?? '');
@@ -121,17 +107,17 @@ public function Plain()
         return trim(Convert::xml2raw($text) ?? '');
     }
 
-    public function scaffoldFormField($title = null, $params = null)
+    public function scaffoldFormField(?string $title = null, array $params = []): ?FormField
     {
         return HTMLEditorField::create($this->name, $title);
     }
 
-    public function scaffoldSearchField($title = null)
+    public function scaffoldSearchField(?string $title = null): ?FormField
     {
         return TextField::create($this->name, $title);
     }
 
-    public function getSchemaValue()
+    public function getSchemaValue(): ?array
     {
         // Form schema format as HTML
         $value = $this->RAW();
@@ -141,7 +127,7 @@ public function getSchemaValue()
         return null;
     }
 
-    public function exists()
+    public function exists(): bool
     {
         // Optimisation: don't process shortcode just for ->exists()
         $value = $this->getValue();
diff --git a/src/ORM/FieldType/DBInt.php b/src/ORM/FieldType/DBInt.php
index 656a98ac393..4a90118757e 100644
--- a/src/ORM/FieldType/DBInt.php
+++ b/src/ORM/FieldType/DBInt.php
@@ -2,9 +2,11 @@
 
 namespace SilverStripe\ORM\FieldType;
 
+use SilverStripe\Forms\FormField;
 use SilverStripe\Forms\NumericField;
 use SilverStripe\ORM\ArrayList;
 use SilverStripe\ORM\DB;
+use SilverStripe\ORM\SS_List;
 use SilverStripe\View\ArrayData;
 
 /**
@@ -12,8 +14,7 @@
  */
 class DBInt extends DBField
 {
-
-    public function __construct($name = null, $defaultVal = 0)
+    public function __construct(?string $name = null, int $defaultVal = 0)
     {
         $this->defaultVal = is_int($defaultVal) ? $defaultVal : 0;
 
@@ -24,7 +25,7 @@ public function __construct($name = null, $defaultVal = 0)
      * Ensure int values are always returned.
      * This is for mis-configured databases that return strings.
      */
-    public function getValue()
+    public function getValue(): ?int
     {
         return (int) $this->value;
     }
@@ -32,12 +33,12 @@ public function getValue()
     /**
      * Returns the number, with commas added as appropriate, eg “1,000”.
      */
-    public function Formatted()
+    public function Formatted(): string
     {
         return number_format($this->value ?? 0.0);
     }
 
-    public function requireField()
+    public function requireField(): void
     {
         $parts = [
             'datatype' => 'int',
@@ -50,7 +51,7 @@ public function requireField()
         DB::require_field($this->tableName, $this->name, $values);
     }
 
-    public function Times()
+    public function Times(): SS_List
     {
         $output = new ArrayList();
         for ($i = 0; $i < $this->value; $i++) {
@@ -60,22 +61,22 @@ public function Times()
         return $output;
     }
 
-    public function Nice()
+    public function Nice(): string
     {
         return sprintf('%d', $this->value);
     }
 
-    public function scaffoldFormField($title = null, $params = null)
+    public function scaffoldFormField(?string $title = null, array $params = []): ?FormField
     {
         return NumericField::create($this->name, $title);
     }
 
-    public function nullValue()
+    public function nullValue(): ?int
     {
         return 0;
     }
 
-    public function prepValueForDB($value)
+    public function prepValueForDB(mixed $value): ?int
     {
         if ($value === true) {
             return 1;
diff --git a/src/ORM/FieldType/DBLocale.php b/src/ORM/FieldType/DBLocale.php
index 9b5e7c61458..aae33bbeefe 100644
--- a/src/ORM/FieldType/DBLocale.php
+++ b/src/ORM/FieldType/DBLocale.php
@@ -9,8 +9,7 @@
  */
 class DBLocale extends DBVarchar
 {
-
-    public function __construct($name = null, $size = 16)
+    public function __construct(?string $name = null, int $size = 16)
     {
         parent::__construct($name, $size);
     }
@@ -18,11 +17,10 @@ public function __construct($name = null, $size = 16)
     /**
      * See {@link getShortName()} and {@link getNativeName()}.
      *
-     * @param Boolean $showNative Show a localized version of the name instead, based on the
+     * @param bool $showNative Show a localized version of the name instead, based on the
      *  field's locale value.
-     * @return String
      */
-    public function Nice($showNative = false)
+    public function Nice(bool $showNative = false): string
     {
         if ($showNative) {
             return $this->getNativeName();
@@ -30,7 +28,7 @@ public function Nice($showNative = false)
         return $this->getShortName();
     }
 
-    public function RFC1766()
+    public function RFC1766(): string
     {
         return i18n::convert_rfc1766($this->value);
     }
@@ -38,18 +36,13 @@ public function RFC1766()
     /**
      * Resolves the locale to a common english-language
      * name through {@link i18n::get_common_locales()}.
-     *
-     * @return string
      */
-    public function getShortName()
+    public function getShortName(): string
     {
         return i18n::getData()->languageName($this->value);
     }
 
-    /**
-     * @return string
-     */
-    public function getLongName()
+    public function getLongName(): string
     {
         return i18n::getData()->localeName($this->value);
     }
@@ -57,10 +50,8 @@ public function getLongName()
     /**
      * Returns the localized name based on the field's value.
      * Example: "de_DE" returns "Deutsch".
-     *
-     * @return string
      */
-    public function getNativeName()
+    public function getNativeName(): string
     {
         $locale = $this->value;
         return i18n::with_locale($locale, function () {
diff --git a/src/ORM/FieldType/DBMoney.php b/src/ORM/FieldType/DBMoney.php
index a9573bc5f1b..04aaa9dd0df 100644
--- a/src/ORM/FieldType/DBMoney.php
+++ b/src/ORM/FieldType/DBMoney.php
@@ -13,25 +13,17 @@
  */
 class DBMoney extends DBComposite
 {
-    /**
-     * @var string $locale
-     */
-    protected $locale = null;
+    protected ?string $locale = null;
 
-    /**
-     * @var array<string,string>
-     */
-    private static $composite_db = [
+    private static array $composite_db = [
         'Currency' => 'Varchar(3)',
         'Amount' => 'Decimal(19,4)'
     ];
 
     /**
      * Get currency formatter
-     *
-     * @return NumberFormatter
      */
-    public function getFormatter()
+    public function getFormatter(): NumberFormatter
     {
         $locale = $this->getLocale();
         $currency = $this->getCurrency();
@@ -43,10 +35,8 @@ public function getFormatter()
 
     /**
      * Get nicely formatted currency (based on current locale)
-     *
-     * @return string
      */
-    public function Nice()
+    public function Nice(): string
     {
         if (!$this->exists()) {
             return null;
@@ -66,10 +56,8 @@ public function Nice()
 
     /**
      * Standard '0.00 CUR' format (non-localised)
-     *
-     * @return string
      */
-    public function getValue()
+    public function getValue(): ?string
     {
         if (!$this->exists()) {
             return null;
@@ -82,39 +70,23 @@ public function getValue()
         return $amount . ' ' . $currency;
     }
 
-    /**
-     * @return string
-     */
-    public function getCurrency()
+    public function getCurrency(): ?string
     {
         return $this->getField('Currency');
     }
 
-    /**
-     * @param string $currency
-     * @param bool $markChanged
-     * @return $this
-     */
-    public function setCurrency($currency, $markChanged = true)
+    public function setCurrency(?string $currency, bool $markChanged = true): static
     {
         $this->setField('Currency', $currency, $markChanged);
         return $this;
     }
 
-    /**
-     * @return float
-     */
-    public function getAmount()
+    public function getAmount(): ?float
     {
         return $this->getField('Amount');
     }
 
-    /**
-     * @param mixed $amount
-     * @param bool $markChanged
-     * @return $this
-     */
-    public function setAmount($amount, $markChanged = true)
+    public function setAmount(mixed $amount, bool $markChanged = true): static
     {
         // Retain nullability to mark this field as empty
         if (isset($amount)) {
@@ -124,49 +96,35 @@ public function setAmount($amount, $markChanged = true)
         return $this;
     }
 
-    /**
-     * @return boolean
-     */
-    public function exists()
+    public function exists(): bool
     {
         return is_numeric($this->getAmount());
     }
 
     /**
      * Determine if this has a non-zero amount
-     *
-     * @return bool
      */
-    public function hasAmount()
+    public function hasAmount(): bool
     {
         $a = $this->getAmount();
         return (!empty($a) && is_numeric($a));
     }
 
-    /**
-     * @param string $locale
-     * @return $this
-     */
-    public function setLocale($locale)
+    public function setLocale(string $locale): static
     {
         $this->locale = $locale;
         return $this;
     }
 
-    /**
-     * @return string
-     */
-    public function getLocale()
+    public function getLocale(): string
     {
         return $this->locale ?: i18n::get_locale();
     }
 
     /**
      * Get currency symbol
-     *
-     * @return string
      */
-    public function getSymbol()
+    public function getSymbol(): string
     {
         return $this->getFormatter()->getSymbol(NumberFormatter::CURRENCY_SYMBOL);
     }
@@ -178,10 +136,8 @@ public function getSymbol()
      * Used by {@link SearchContext}, {@link ModelAdmin}, {@link DataObject::scaffoldFormFields()}
      *
      * @param string $title Optional. Localized title of the generated instance
-     * @param array $params
-     * @return FormField
      */
-    public function scaffoldFormField($title = null, $params = null)
+    public function scaffoldFormField(?string $title = null, array $params = []): ?FormField
     {
         return MoneyField::create($this->getName(), $title)
             ->setLocale($this->getLocale());
diff --git a/src/ORM/FieldType/DBMultiEnum.php b/src/ORM/FieldType/DBMultiEnum.php
index 93cccab8e4e..1e55b5b51c7 100644
--- a/src/ORM/FieldType/DBMultiEnum.php
+++ b/src/ORM/FieldType/DBMultiEnum.php
@@ -4,6 +4,7 @@
 
 use SilverStripe\Core\Config\Config;
 use SilverStripe\Forms\CheckboxSetField;
+use SilverStripe\Forms\MultiSelectField;
 use SilverStripe\ORM\Connect\MySQLDatabase;
 use SilverStripe\ORM\DB;
 
@@ -33,7 +34,7 @@ public function __construct($name = null, $enum = null, $default = null)
         }
     }
 
-    public function requireField()
+    public function requireField(): void
     {
         $charset = Config::inst()->get(MySQLDatabase::class, 'charset');
         $collation = Config::inst()->get(MySQLDatabase::class, 'collation');
@@ -54,18 +55,15 @@ public function requireField()
 
 
     /**
-     * Return a {@link CheckboxSetField} suitable for editing this field
-     *
-     * @param string $title
-     * @param string $name
-     * @param bool $hasEmpty
-     * @param string $value
-     * @param string $emptyString
-     * @return CheckboxSetField
+     * Return a form field suitable for editing this field
      */
-    public function formField($title = null, $name = null, $hasEmpty = false, $value = '', $emptyString = null)
-    {
-
+    public function formField(
+        ?string $title = null,
+        ?string $name = null,
+        bool $hasEmpty = false,
+        ?string $value = '',
+        ?string $emptyString = null
+    ): MultiSelectField {
         if (!$title) {
             $title = $this->name;
         }
@@ -73,6 +71,6 @@ public function formField($title = null, $name = null, $hasEmpty = false, $value
             $name = $this->name;
         }
 
-        return new CheckboxSetField($name, $title, $this->enumValues($hasEmpty), $value);
+        return CheckboxSetField::create($name, $title, $this->enumValues($hasEmpty), $value);
     }
 }
diff --git a/src/ORM/FieldType/DBPercentage.php b/src/ORM/FieldType/DBPercentage.php
index 1abf613a26a..cf703e8c140 100644
--- a/src/ORM/FieldType/DBPercentage.php
+++ b/src/ORM/FieldType/DBPercentage.php
@@ -2,6 +2,8 @@
 
 namespace SilverStripe\ORM\FieldType;
 
+use SilverStripe\View\ViewableData;
+
 /**
  * Represents a decimal field from 0-1 containing a percentage value.
  *
@@ -15,14 +17,10 @@
  */
 class DBPercentage extends DBDecimal
 {
-
     /**
      * Create a new Decimal field.
-     *
-     * @param string $name
-     * @param int $precision
      */
-    public function __construct($name = null, $precision = 4)
+    public function __construct(?string $name = null, int $precision = 4)
     {
         if (!$precision) {
             $precision = 4;
@@ -34,18 +32,18 @@ public function __construct($name = null, $precision = 4)
     /**
      * Returns the number, expressed as a percentage. For example, “36.30%”
      */
-    public function Nice()
+    public function Nice(): string
     {
         return number_format($this->value * 100, $this->decimalSize - 2) . '%';
     }
 
-    public function saveInto($dataObject)
+    public function saveInto(ViewableData $model): void
     {
-        parent::saveInto($dataObject);
+        parent::saveInto($model);
 
         $fieldName = $this->name;
-        if ($fieldName && $dataObject->$fieldName > 1.0) {
-            $dataObject->__set($fieldName, 1.0);
+        if ($fieldName && $model->$fieldName > 1.0) {
+            $model->__set($fieldName, 1.0);
         }
     }
 }
diff --git a/src/ORM/FieldType/DBPolymorphicForeignKey.php b/src/ORM/FieldType/DBPolymorphicForeignKey.php
index a70be359cc7..8e5616635f3 100644
--- a/src/ORM/FieldType/DBPolymorphicForeignKey.php
+++ b/src/ORM/FieldType/DBPolymorphicForeignKey.php
@@ -2,21 +2,23 @@
 
 namespace SilverStripe\ORM\FieldType;
 
+use SilverStripe\Forms\FormField;
 use SilverStripe\ORM\DataObject;
+use SilverStripe\View\ViewableData;
 
 /**
  * A special ForeignKey class that handles relations with arbitrary class types
  */
 class DBPolymorphicForeignKey extends DBComposite
 {
-    private static $index = true;
+    private static bool $index = true;
 
-    private static $composite_db = [
+    private static array $composite_db = [
         'ID' => 'Int',
         'Class' => "DBClassName('" . DataObject::class . "', ['index' => false])"
     ];
 
-    public function scaffoldFormField($title = null, $params = null)
+    public function scaffoldFormField(?string $title = null, array $params = []): ?FormField
     {
         // Don't provide scaffolded form field generation - Scaffolding should be performed on
         // the has_many end, or set programmatically.
@@ -28,7 +30,7 @@ public function scaffoldFormField($title = null, $params = null)
      *
      * @return string Name of a subclass of DataObject
      */
-    public function getClassValue()
+    public function getClassValue(): ?string
     {
         return $this->getField('Class');
     }
@@ -37,35 +39,29 @@ public function getClassValue()
      * Set the value of the "Class" this key points to
      *
      * @param string $value Name of a subclass of DataObject
-     * @param boolean $markChanged Mark this field as changed?
      */
-    public function setClassValue($value, $markChanged = true)
+    public function setClassValue(string $value, bool $markChanged = true)
     {
         $this->setField('Class', $value, $markChanged);
     }
 
     /**
      * Gets the value of the "ID" this key points to
-     *
-     * @return integer
      */
-    public function getIDValue()
+    public function getIDValue(): ?int
     {
         return $this->getField('ID');
     }
 
     /**
      * Sets the value of the "ID" this key points to
-     *
-     * @param integer $value
-     * @param boolean $markChanged Mark this field as changed?
      */
-    public function setIDValue($value, $markChanged = true)
+    public function setIDValue(int $value, bool $markChanged = true)
     {
         $this->setField('ID', $value, $markChanged);
     }
 
-    public function setValue($value, $record = null, $markChanged = true)
+    public function setValue(mixed $value, null|array|ViewableData $record = null, bool $markChanged = true): static
     {
         // Map dataobject value to array
         if ($value instanceof DataObject) {
@@ -75,10 +71,10 @@ public function setValue($value, $record = null, $markChanged = true)
             ];
         }
 
-        parent::setValue($value, $record, $markChanged);
+        return parent::setValue($value, $record, $markChanged);
     }
 
-    public function getValue()
+    public function getValue(): ?DataObject
     {
         $id = $this->getIDValue();
         $class = $this->getClassValue();
diff --git a/src/ORM/FieldType/DBPrimaryKey.php b/src/ORM/FieldType/DBPrimaryKey.php
index 7e9a207e240..9a6f7023811 100644
--- a/src/ORM/FieldType/DBPrimaryKey.php
+++ b/src/ORM/FieldType/DBPrimaryKey.php
@@ -2,64 +2,59 @@
 
 namespace SilverStripe\ORM\FieldType;
 
+use SilverStripe\Forms\FormField;
 use SilverStripe\ORM\DataObject;
 use SilverStripe\ORM\DB;
+use SilverStripe\View\ViewableData;
 
 /**
  * A special type Int field used for primary keys.
  */
 class DBPrimaryKey extends DBInt
 {
-    /**
-     * @var DataObject
-     */
-    protected $object;
+    protected ?DataObject $object;
 
     private static $default_search_filter_class = 'ExactMatchFilter';
 
+    protected bool $autoIncrement = true;
+
     /**
-     * @var bool
+     * @param DataObject $object The object that this is primary key for (should have a relation with $name)
      */
-    protected $autoIncrement = true;
+    public function __construct(?string $name, ?DataObject $object = null)
+    {
+        $this->object = $object;
+        parent::__construct($name);
+    }
 
-    public function setAutoIncrement($autoIncrement)
+    public function setAutoIncrement(bool $autoIncrement): static
     {
         $this->autoIncrement = $autoIncrement;
         return $this;
     }
 
-    public function getAutoIncrement()
+    public function getAutoIncrement(): bool
     {
         return $this->autoIncrement;
     }
 
-    public function requireField()
+    public function requireField(): void
     {
         $spec = DB::get_schema()->IdColumn(false, $this->getAutoIncrement());
         DB::require_field($this->getTable(), $this->getName(), $spec);
     }
 
-    /**
-     * @param string $name
-     * @param DataObject $object The object that this is primary key for (should have a relation with $name)
-     */
-    public function __construct($name, $object = null)
-    {
-        $this->object = $object;
-        parent::__construct($name);
-    }
-
-    public function scaffoldFormField($title = null, $params = null)
+    public function scaffoldFormField(?string $title = null, array $params = []): ?FormField
     {
         return null;
     }
 
-    public function scaffoldSearchField($title = null)
+    public function scaffoldSearchField(?string $title = null): ?FormField
     {
-        parent::scaffoldFormField($title);
+        return parent::scaffoldFormField($title);
     }
 
-    public function setValue($value, $record = null, $markChanged = true)
+    public function setValue(mixed $value, null|array|ViewableData $record = null, bool $markChanged = true): static
     {
         parent::setValue($value, $record, $markChanged);
 
diff --git a/src/ORM/FieldType/DBString.php b/src/ORM/FieldType/DBString.php
index 875936d5650..aed498b6ec3 100644
--- a/src/ORM/FieldType/DBString.php
+++ b/src/ORM/FieldType/DBString.php
@@ -7,10 +7,7 @@
  */
 abstract class DBString extends DBField
 {
-    /**
-     * @var array
-     */
-    private static $casting = [
+    private static array $casting = [
         'LimitCharacters' => 'Text',
         'LimitCharactersToClosestWord' => 'Text',
         'LimitWordCount' => 'Text',
@@ -33,16 +30,14 @@ public function __construct($name = null, $options = [])
     /**
      * Update the optional parameters for this field.
      *
-     * @param array $options Array of options
      * The options allowed are:
      *   <ul><li>"nullifyEmpty"
      *       This is a boolean flag.
      *       True (the default) means that empty strings are automatically converted to nulls to be stored in
      *       the database. Set it to false to ensure that nulls and empty strings are kept intact in the database.
      *   </li></ul>
-     * @return $this
      */
-    public function setOptions(array $options = [])
+    public function setOptions(array $options = []): static
     {
         parent::setOptions($options);
 
@@ -63,9 +58,9 @@ public function setOptions(array $options = [])
      * @param $value boolean True if empty strings are to be converted to null
      * @return $this
      */
-    public function setNullifyEmpty($value)
+    public function setNullifyEmpty(bool $value): static
     {
-        $this->options['nullifyEmpty'] = (bool) $value;
+        $this->options['nullifyEmpty'] = $value;
         return $this;
     }
 
@@ -75,23 +70,19 @@ public function setNullifyEmpty($value)
      *
      * @return boolean True if empty strings are to be converted to null
      */
-    public function getNullifyEmpty()
+    public function getNullifyEmpty(): bool
     {
         return !empty($this->options['nullifyEmpty']);
     }
 
-    /**
-     * (non-PHPdoc)
-     * @see DBField::exists()
-     */
-    public function exists()
+    public function exists(): bool
     {
         $value = $this->RAW();
         // All truthy values and non-empty strings exist ('0' but not (int)0)
         return $value || (is_string($value) && strlen($value ?? ''));
     }
 
-    public function prepValueForDB($value)
+    public function prepValueForDB(mixed $value): ?string
     {
         // Cast non-empty value
         if (is_scalar($value) && strlen($value ?? '')) {
@@ -105,10 +96,7 @@ public function prepValueForDB($value)
         return '';
     }
 
-    /**
-     * @return string
-     */
-    public function forTemplate()
+    public function forTemplate(): string
     {
         return nl2br(parent::forTemplate() ?? '');
     }
@@ -120,9 +108,8 @@ public function forTemplate()
      *
      * @param int $limit Number of characters to limit by
      * @param string|false $add Ellipsis to add to the end of truncated string
-     * @return string
      */
-    public function LimitCharacters($limit = 20, $add = false)
+    public function LimitCharacters(int $limit = 20, string|false $add = false): string
     {
         $value = $this->Plain();
         if (mb_strlen($value ?? '') <= $limit) {
@@ -140,7 +127,7 @@ public function LimitCharacters($limit = 20, $add = false)
      * @param string|false $add Ellipsis to add to the end of truncated string
      * @return string Plain text value with limited characters
      */
-    public function LimitCharactersToClosestWord($limit = 20, $add = false)
+    public function LimitCharactersToClosestWord(int $limit = 20, string|false $add = false): string
     {
         // Safely convert to plain text
         $value = $this->Plain();
@@ -169,11 +156,9 @@ public function LimitCharactersToClosestWord($limit = 20, $add = false)
      * Limit this field's content by a number of words.
      *
      * @param int $numWords Number of words to limit by.
-     * @param false $add Ellipsis to add to the end of truncated string.
-     *
-     * @return string
+     * @param string|false $add Ellipsis to add to the end of truncated string.
      */
-    public function LimitWordCount($numWords = 26, $add = false)
+    public function LimitWordCount(int $numWords = 26, string|false $add = false): string
     {
         $value = $this->Plain();
         $words = explode(' ', $value ?? '');
@@ -191,7 +176,7 @@ public function LimitWordCount($numWords = 26, $add = false)
      *
      * @return string Text with lowercase (HTML for some subclasses)
      */
-    public function LowerCase()
+    public function LowerCase(): string
     {
         return mb_strtolower($this->RAW() ?? '');
     }
@@ -201,28 +186,23 @@ public function LowerCase()
      *
      * @return string Text with uppercase (HTML for some subclasses)
      */
-    public function UpperCase()
+    public function UpperCase(): string
     {
         return mb_strtoupper($this->RAW() ?? '');
     }
 
     /**
      * Plain text version of this string
-     *
-     * @return string Plain text
      */
-    public function Plain()
+    public function Plain(): string
     {
         return trim($this->RAW() ?? '');
     }
 
     /**
      * Swap add for defaultEllipsis if need be
-     * @param string $string
-     * @param false|string $add
-     * @return string
      */
-    private function addEllipsis(string $string, $add): string
+    private function addEllipsis(string $string, string|false $add): string
     {
         if ($add === false) {
             $add = $this->defaultEllipsis();
@@ -233,7 +213,6 @@ private function addEllipsis(string $string, $add): string
 
     /**
      * Get the default string to indicate that a string was cut off.
-     * @return string
      */
     public function defaultEllipsis(): string
     {
diff --git a/src/ORM/FieldType/DBText.php b/src/ORM/FieldType/DBText.php
index f0025258525..97169cf1a32 100644
--- a/src/ORM/FieldType/DBText.php
+++ b/src/ORM/FieldType/DBText.php
@@ -5,6 +5,7 @@
 use InvalidArgumentException;
 use SilverStripe\Core\Config\Config;
 use SilverStripe\Core\Convert;
+use SilverStripe\Forms\FormField;
 use SilverStripe\Forms\NullableField;
 use SilverStripe\Forms\TextareaField;
 use SilverStripe\Forms\TextField;
@@ -27,8 +28,7 @@
  */
 class DBText extends DBString
 {
-
-    private static $casting = [
+    private static array $casting = [
         'BigSummary' => 'Text',
         'ContextSummary' => 'HTMLFragment', // Always returns HTML as it contains formatting and highlighting
         'FirstParagraph' => 'Text',
@@ -42,11 +42,7 @@ class DBText extends DBString
      */
     private static array $summary_sentence_separators = ['.', '?', '!'];
 
-    /**
-     * (non-PHPdoc)
-     * @see DBField::requireField()
-     */
-    public function requireField()
+    public function requireField(): void
     {
         $charset = Config::inst()->get(MySQLDatabase::class, 'charset');
         $collation = Config::inst()->get(MySQLDatabase::class, 'collation');
@@ -71,9 +67,8 @@ public function requireField()
      * Limit sentences, can be controlled by passing an integer.
      *
      * @param int $maxSentences The amount of sentences you want.
-     * @return string
      */
-    public function LimitSentences($maxSentences = 2)
+    public function LimitSentences(int $maxSentences = 2): string
     {
         if (!is_numeric($maxSentences)) {
             throw new InvalidArgumentException("Text::LimitSentence() expects one numeric argument");
@@ -107,22 +102,16 @@ public function LimitSentences($maxSentences = 2)
 
     /**
      * Return the first string that finishes with a period (.) in this text.
-     *
-     * @return string
      */
-    public function FirstSentence()
+    public function FirstSentence(): string
     {
         return $this->LimitSentences(1);
     }
 
     /**
      * Builds a basic summary, up to a maximum number of words
-     *
-     * @param int $maxWords
-     * @param string|false $add
-     * @return string
      */
-    public function Summary($maxWords = 50, $add = false)
+    public function Summary(int $maxWords = 50, string|false $add = false): string
     {
         // Get plain-text version
         $value = $this->Plain();
@@ -171,10 +160,8 @@ public function Summary($maxWords = 50, $add = false)
 
     /**
      * Get first paragraph
-     *
-     * @return string
      */
-    public function FirstParagraph()
+    public function FirstParagraph(): string
     {
         $value = $this->Plain();
         if (empty($value)) {
@@ -193,17 +180,15 @@ public function FirstParagraph()
      * @param int $characters Number of characters in the summary
      * @param string $keywords Supplied string ("keywords"). Will fall back to 'Search' querystring arg.
      * @param bool $highlight Add a highlight <mark> element around search query?
-     * @param string|false $prefix Prefix text
-     * @param string|false $suffix Suffix text
      * @return string HTML string with context
      */
     public function ContextSummary(
-        $characters = 500,
-        $keywords = null,
-        $highlight = true,
-        $prefix = false,
-        $suffix = false
-    ) {
+        int $characters = 500,
+        ?string $keywords = null,
+        bool $highlight = true,
+        string|false $prefix = false,
+        string|false $suffix = false
+    ): string {
 
         if (!$keywords) {
             // Use the default "Search" request variable (from SearchForm)
@@ -267,7 +252,7 @@ public function ContextSummary(
         return nl2br($summary ?? '');
     }
 
-    public function scaffoldFormField($title = null, $params = null)
+    public function scaffoldFormField(?string $title = null, array $params = []): ?FormField
     {
         if (!$this->nullifyEmpty) {
             // Allow the user to select if it's null instead of automatically assuming empty string is
@@ -277,7 +262,7 @@ public function scaffoldFormField($title = null, $params = null)
         return TextareaField::create($this->name, $title);
     }
 
-    public function scaffoldSearchField($title = null)
+    public function scaffoldSearchField(?string $title = null): ?FormField
     {
         return new TextField($this->name, $title);
     }
diff --git a/src/ORM/FieldType/DBTime.php b/src/ORM/FieldType/DBTime.php
index 6b1813eea5a..9c64359c349 100644
--- a/src/ORM/FieldType/DBTime.php
+++ b/src/ORM/FieldType/DBTime.php
@@ -4,11 +4,13 @@
 
 use IntlDateFormatter;
 use InvalidArgumentException;
+use SilverStripe\Forms\FormField;
 use SilverStripe\Forms\TimeField;
 use SilverStripe\i18n\i18n;
 use SilverStripe\ORM\DB;
 use SilverStripe\Security\Member;
 use SilverStripe\Security\Security;
+use SilverStripe\View\ViewableData;
 
 /**
  * Represents a column in the database with the type 'Time'.
@@ -25,9 +27,9 @@ class DBTime extends DBField
     /**
      * Standard ISO format string for time in CLDR standard format
      */
-    const ISO_TIME = 'HH:mm:ss';
+    public const ISO_TIME = 'HH:mm:ss';
 
-    public function setValue($value, $record = null, $markChanged = true)
+    public function setValue(mixed $value, null|array|ViewableData $record = null, bool $markChanged = true): static
     {
         $value = $this->parseTime($value);
         if ($value === false) {
@@ -42,10 +44,9 @@ public function setValue($value, $record = null, $markChanged = true)
     /**
      * Parse timestamp or iso8601-ish date into standard iso8601 format
      *
-     * @param mixed $value
      * @return string|null|false Formatted time, null if empty but valid, or false if invalid
      */
-    protected function parseTime($value)
+    protected function parseTime(mixed $value): string|null|false
     {
         // Skip empty values
         if (empty($value) && !is_numeric($value)) {
@@ -73,24 +74,19 @@ protected function parseTime($value)
 
     /**
      * Get date / time formatter for the current locale
-     *
-     * @param int $timeLength
-     * @return IntlDateFormatter
      */
-    public function getFormatter($timeLength = IntlDateFormatter::MEDIUM)
+    public function getFormatter(int $timeLength = IntlDateFormatter::MEDIUM): IntlDateFormatter
     {
         return IntlDateFormatter::create(i18n::get_locale(), IntlDateFormatter::NONE, $timeLength);
     }
 
     /**
      * Returns the date in the localised short format
-     *
-     * @return string
      */
-    public function Short()
+    public function Short(): string
     {
         if (!$this->value) {
-            return null;
+            return '';
         }
         $formatter = $this->getFormatter(IntlDateFormatter::SHORT);
         return $formatter->format($this->getTimestamp());
@@ -99,13 +95,11 @@ public function Short()
     /**
      * Returns the standard localised medium time
      * e.g. "3:15pm"
-     *
-     * @return string
      */
-    public function Nice()
+    public function Nice(): string
     {
         if (!$this->value) {
-            return null;
+            return '';
         }
         $formatter = $this->getFormatter();
         return $formatter->format($this->getTimestamp());
@@ -114,20 +108,19 @@ public function Nice()
     /**
      * Return the time using a particular formatting string.
      *
-     * @param string $format Format code string. See https://unicode-org.github.io/icu/userguide/format_parse/datetime
-     * @return string The time in the requested format
+     * See https://unicode-org.github.io/icu/userguide/format_parse/datetime for valid formats
      */
-    public function Format($format)
+    public function Format(string $format): string
     {
         if (!$this->value) {
-            return null;
+            return '';
         }
         $formatter = $this->getFormatter();
         $formatter->setPattern($format);
         return $formatter->format($this->getTimestamp());
     }
 
-    public function requireField()
+    public function requireField(): void
     {
         $parts = [
             'datatype' => 'time',
@@ -140,18 +133,15 @@ public function requireField()
         DB::require_field($this->tableName, $this->name, $values);
     }
 
-    public function scaffoldFormField($title = null, $params = null)
+    public function scaffoldFormField(?string $title = null, array $params = []): ?FormField
     {
         return TimeField::create($this->name, $title);
     }
 
     /**
      * Return a time formatted as per a CMS user's settings.
-     *
-     * @param Member $member
-     * @return string A time formatted as per user-defined settings.
      */
-    public function FormatFromSettings($member = null)
+    public function FormatFromSettings(?Member $member = null): string
     {
         if (!$member) {
             $member = Security::getCurrentUser();
@@ -169,20 +159,16 @@ public function FormatFromSettings($member = null)
 
     /**
      * Get standard ISO time format string
-     *
-     * @return string
      */
-    public function getISOFormat()
+    public function getISOFormat(): string
     {
         return DBTime::ISO_TIME;
     }
 
     /**
      * Get unix timestamp for this time
-     *
-     * @return int
      */
-    public function getTimestamp()
+    public function getTimestamp(): int
     {
         if ($this->value) {
             return strtotime($this->value ?? '');
diff --git a/src/ORM/FieldType/DBVarchar.php b/src/ORM/FieldType/DBVarchar.php
index d38f4d462c2..3081ad34be0 100644
--- a/src/ORM/FieldType/DBVarchar.php
+++ b/src/ORM/FieldType/DBVarchar.php
@@ -3,6 +3,7 @@
 namespace SilverStripe\ORM\FieldType;
 
 use SilverStripe\Core\Config\Config;
+use SilverStripe\Forms\FormField;
 use SilverStripe\Forms\NullableField;
 use SilverStripe\Forms\TextField;
 use SilverStripe\ORM\Connect\MySQLDatabase;
@@ -17,18 +18,15 @@
  */
 class DBVarchar extends DBString
 {
-
-    private static $casting = [
+    private static array $casting = [
         'Initial' => 'Text',
         'URL' => 'Text',
     ];
 
     /**
      * Max size of this field
-     *
-     * @var int
      */
-    protected $size;
+    protected int $size;
 
     /**
      * Construct a new short text field
@@ -38,7 +36,7 @@ class DBVarchar extends DBString
      * @param array $options Optional parameters, e.g. array("nullifyEmpty"=>false).
      *                       See {@link StringField::setOptions()} for information on the available options
      */
-    public function __construct($name = null, $size = 255, $options = [])
+    public function __construct(?string $name = null, int $size = 255, array $options = [])
     {
         $this->size = $size ? $size : 255;
         parent::__construct($name, $options);
@@ -53,16 +51,12 @@ public function __construct($name = null, $size = 255, $options = [])
      *
      * @return int The size of the field
      */
-    public function getSize()
+    public function getSize(): int
     {
         return $this->size;
     }
 
-    /**
-     * (non-PHPdoc)
-     * @see DBField::requireField()
-     */
-    public function requireField()
+    public function requireField(): void
     {
         $charset = Config::inst()->get(MySQLDatabase::class, 'charset');
         $collation = Config::inst()->get(MySQLDatabase::class, 'collation');
@@ -85,24 +79,20 @@ public function requireField()
 
     /**
      * Return the first letter of the string followed by a .
-     *
-     * @return string
      */
-    public function Initial()
+    public function Initial(): string
     {
         if ($this->exists()) {
             $value = $this->RAW();
             return $value[0] . '.';
         }
-        return null;
+        return '';
     }
 
     /**
      * Ensure that the given value is an absolute URL.
-     *
-     * @return string
      */
-    public function URL()
+    public function URL(): string
     {
         $value = $this->RAW();
         if (preg_match('#^[a-zA-Z]+://#', $value ?? '')) {
@@ -113,14 +103,13 @@ public function URL()
 
     /**
      * Return the value of the field in rich text format
-     * @return string
      */
-    public function RTF()
+    public function RTF(): string
     {
         return str_replace("\n", '\par ', $this->RAW() ?? '');
     }
 
-    public function scaffoldFormField($title = null, $params = null)
+    public function scaffoldFormField(?string $title = null, array $params = []): ?FormField
     {
         // Set field with appropriate size
         $field = TextField::create($this->name, $title);
diff --git a/src/ORM/FieldType/DBYear.php b/src/ORM/FieldType/DBYear.php
index 55a974d1990..04618cae339 100644
--- a/src/ORM/FieldType/DBYear.php
+++ b/src/ORM/FieldType/DBYear.php
@@ -3,6 +3,7 @@
 namespace SilverStripe\ORM\FieldType;
 
 use SilverStripe\Forms\DropdownField;
+use SilverStripe\Forms\FormField;
 use SilverStripe\ORM\DB;
 
 /**
@@ -10,15 +11,14 @@
  */
 class DBYear extends DBField
 {
-
-    public function requireField()
+    public function requireField(): void
     {
         $parts = ['datatype' => 'year', 'precision' => 4, 'arrayValue' => $this->arrayValue];
         $values = ['type' => 'year', 'parts' => $parts];
         DB::require_field($this->tableName, $this->name, $values);
     }
 
-    public function scaffoldFormField($title = null, $params = null)
+    public function scaffoldFormField(?string $title = null, array $params = []): ?FormField
     {
         $selectBox = DropdownField::create($this->name, $title);
         $selectBox->setSource($this->getDefaultOptions());
@@ -31,11 +31,10 @@ public function scaffoldFormField($title = null, $params = null)
      * input values. Starts by default at the current year,
      * and counts back to 1900.
      *
-     * @param int|bool $start starting date to count down from
-     * @param int|bool $end end date to count down to
-     * @return array
+     * @param int|null $start starting date to count down from
+     * @param int|null $end end date to count down to
      */
-    private function getDefaultOptions($start = null, $end = null)
+    private function getDefaultOptions(?int $start = null, ?int $end = null): array
     {
         if (!$start) {
             $start = (int)date('Y');
diff --git a/src/ORM/ListDecorator.php b/src/ORM/ListDecorator.php
index b063acb7481..956cfd0d1d1 100644
--- a/src/ORM/ListDecorator.php
+++ b/src/ORM/ListDecorator.php
@@ -112,7 +112,7 @@ public function getIterator(): Traversable
         return $this->list->getIterator();
     }
 
-    public function exists()
+    public function exists(): bool
     {
         return $this->list->exists();
     }
@@ -140,7 +140,7 @@ public function Count(): int
         return $this->list->count();
     }
 
-    public function forTemplate()
+    public function forTemplate(): string
     {
         return $this->list->forTemplate();
     }
@@ -313,7 +313,7 @@ public function exclude()
         return $this->list->exclude(...func_get_args());
     }
 
-    public function debug()
+    public function debug(): string
     {
         return $this->list->debug();
     }
diff --git a/src/View/ArrayData.php b/src/View/ArrayData.php
index 819ad8f75ac..c107fd060c8 100644
--- a/src/View/ArrayData.php
+++ b/src/View/ArrayData.php
@@ -70,13 +70,10 @@ public function toMap()
      *
      * If the value is an associative array, it will likewise be
      * converted recursively to an ArrayData.
-     *
-     * @param string $field
-     * @return mixed
      */
-    public function getField($field)
+    public function getField(string $fieldName): mixed
     {
-        $value = $this->array[$field];
+        $value = $this->array[$fieldName];
         if (is_object($value) && !($value instanceof ViewableData) && !is_iterable($value)) {
             return new ArrayData($value);
         } elseif (ArrayLib::is_associative($value)) {
@@ -87,14 +84,10 @@ public function getField($field)
     }
     /**
     * Add or set a field on this object.
-    *
-    * @param string $field
-    * @param mixed $value
-    * @return $this
     */
-    public function setField($field, $value)
+    public function setField(string $fieldName, mixed $value): static
     {
-        $this->array[$field] = $value;
+        $this->array[$fieldName] = $value;
         return $this;
     }
 
@@ -104,9 +97,9 @@ public function setField($field, $value)
      * @param string $field Field Key
      * @return bool
      */
-    public function hasField($field)
+    public function hasField(string $fieldName): bool
     {
-        return isset($this->array[$field]);
+        return isset($this->array[$fieldName]);
     }
 
     /**
diff --git a/src/View/Parsers/HTMLValue.php b/src/View/Parsers/HTMLValue.php
index 76b5ebc17af..c8c0779b34e 100644
--- a/src/View/Parsers/HTMLValue.php
+++ b/src/View/Parsers/HTMLValue.php
@@ -46,10 +46,7 @@ public function setContent($content)
         return false;
     }
 
-    /**
-     * @return string
-     */
-    public function getContent()
+    public function getContent(): string
     {
         $document = $this->getDocument();
         if (!$document) {
@@ -98,7 +95,7 @@ public function getContent()
     }
 
     /** @see HTMLValue::getContent() */
-    public function forTemplate()
+    public function forTemplate(): string
     {
         return $this->getContent();
     }
diff --git a/src/View/Parsers/ShortcodeParser.php b/src/View/Parsers/ShortcodeParser.php
index 2031a1645f8..bfade4d124d 100644
--- a/src/View/Parsers/ShortcodeParser.php
+++ b/src/View/Parsers/ShortcodeParser.php
@@ -733,6 +733,6 @@ function ($matches) use ($tags, $parser) {
 
         $this->extend('onAfterParse', $content);
 
-        return $content;
+        return $content ?? '';
     }
 }
diff --git a/src/View/SSTemplateParser.peg b/src/View/SSTemplateParser.peg
index 0f15460f97b..a7068859f95 100644
--- a/src/View/SSTemplateParser.peg
+++ b/src/View/SSTemplateParser.peg
@@ -288,7 +288,7 @@ class SSTemplateParser extends Parser implements TemplateParser
             $arguments = $sub['Call']['CallArguments']['php'];
             $res['php'] .= "->$method('$property', [$arguments], true)";
         } else {
-            $res['php'] .= "->$method('$property', null, true)";
+            $res['php'] .= "->$method('$property', [], true)";
         }
     }
 
diff --git a/src/View/SSTemplateParser.php b/src/View/SSTemplateParser.php
index bafe80e4be4..a19c91bbf78 100644
--- a/src/View/SSTemplateParser.php
+++ b/src/View/SSTemplateParser.php
@@ -779,7 +779,7 @@ function Lookup_AddLookupStep(&$res, $sub, $method)
             $arguments = $sub['Call']['CallArguments']['php'];
             $res['php'] .= "->$method('$property', [$arguments], true)";
         } else {
-            $res['php'] .= "->$method('$property', null, true)";
+            $res['php'] .= "->$method('$property', [], true)";
         }
     }
 
@@ -1886,6 +1886,8 @@ function PresenceCheck_Argument(&$res, $sub)
             $res['php'] .= '((bool)'.$sub['php'].')';
         } else {
             $php = ($sub['ArgumentMode'] == 'default' ? $sub['lookup_php'] : $sub['php']);
+            // TODO: kinda hacky - maybe we need a way to pass state down the parse chain so
+            // Lookup_LastLookupStep and Argument_BareWord can produce hasValue instead of XML_val
             $res['php'] .= str_replace('$$FINAL', 'hasValue', $php ?? '');
         }
     }
@@ -5290,6 +5292,8 @@ function Text__finalise(&$res)
         $text = stripslashes($text ?? '');
         $text = addcslashes($text ?? '', '\'\\');
 
+        // TODO: This is pretty ugly & gets applied on all files not just html. I wonder if we can make this
+        // non-dynamically calculated
         $code = <<<'EOC'
 (\SilverStripe\View\SSViewer::getRewriteHashLinksDefault()
     ? \SilverStripe\Core\Convert::raw2att( preg_replace("/^(\\/)+/", "/", $_SERVER['REQUEST_URI'] ) )
@@ -5328,7 +5332,8 @@ public function compileString($string, $templateName = "", $includeDebuggingComm
 
             $this->includeDebuggingComments = $includeDebuggingComments;
 
-            // Ignore UTF8 BOM at beginning of string.
+            // Ignore UTF8 BOM at beginning of string. TODO: Confirm this is needed, make sure SSViewer handles UTF
+            // (and other encodings) properly
             if (substr($string ?? '', 0, 3) == pack("CCC", 0xef, 0xbb, 0xbf)) {
                 $this->pos = 3;
             }
diff --git a/src/View/ViewableData.php b/src/View/ViewableData.php
index d2e7dbccd02..29831960738 100644
--- a/src/View/ViewableData.php
+++ b/src/View/ViewableData.php
@@ -2,29 +2,23 @@
 
 namespace SilverStripe\View;
 
-use ArrayIterator;
 use Exception;
 use InvalidArgumentException;
-use IteratorAggregate;
 use LogicException;
 use ReflectionMethod;
-use ReflectionObject;
 use ReflectionProperty;
 use SilverStripe\Core\ClassInfo;
-use SilverStripe\Core\Config\Config;
 use SilverStripe\Core\Config\Configurable;
 use SilverStripe\Core\Convert;
 use SilverStripe\Core\Extensible;
 use SilverStripe\Core\Injector\Injectable;
 use SilverStripe\Core\Injector\Injector;
 use SilverStripe\Dev\Debug;
-use SilverStripe\Dev\Deprecation;
 use SilverStripe\ORM\ArrayLib;
 use SilverStripe\ORM\ArrayList;
 use SilverStripe\ORM\FieldType\DBField;
 use SilverStripe\ORM\FieldType\DBHTMLText;
 use SilverStripe\View\SSViewer;
-use Traversable;
 use UnexpectedValueException;
 
 /**
@@ -34,7 +28,7 @@
  * is provided and automatically escaped by ViewableData. Any class that needs to be available to a view (controllers,
  * {@link DataObject}s, page controls) should inherit from this class.
  */
-class ViewableData implements IteratorAggregate
+class ViewableData
 {
     use Extensible {
         defineMethods as extensibleDefineMethods;
@@ -50,27 +44,18 @@ class ViewableData implements IteratorAggregate
      *     'FieldName' => 'ClassToCastTo(Arguments)'
      * );
      * </code>
-     *
-     * @var array
-     * @config
      */
-    private static $casting = [
+    private static array $casting = [
         'CSSClasses' => 'Varchar'
     ];
 
     /**
      * The default object to cast scalar fields to if casting information is not specified, and casting to an object
      * is required.
-     *
-     * @var string
-     * @config
      */
-    private static $default_cast = 'Text';
+    private static string $default_cast = 'Text';
 
-    /**
-     * @var array
-     */
-    private static $casting_cache = [];
+    private static array $casting_cache = [];
 
     /**
      * Acts as a PHP 8.2+ compliant replacement for dynamic properties
@@ -81,20 +66,12 @@ class ViewableData implements IteratorAggregate
 
     /**
      * A failover object to attempt to get data from if it is not present on this object.
-     *
-     * @var ViewableData
      */
-    protected $failover;
+    protected ?ViewableData $failover = null;
 
-    /**
-     * @var ViewableData
-     */
-    protected $customisedObject;
+    protected ?ViewableData $customisedObject = null;
 
-    /**
-     * @var array
-     */
-    private $objCache = [];
+    private array $objCache = [];
 
     public function __construct()
     {
@@ -108,11 +85,8 @@ public function __construct()
      * Check if a field exists on this object or its failover.
      * Note that, unlike the core isset() implementation, this will return true if the property is defined
      * and set to null.
-     *
-     * @param string $property
-     * @return bool
      */
-    public function __isset($property)
+    public function __isset(string $property): bool
     {
         // getField() isn't a field-specific getter and shouldn't be treated as such
         if (strtolower($property ?? '') !== 'field' && $this->hasMethod("get$property")) {
@@ -131,11 +105,8 @@ public function __isset($property)
     /**
      * Get the value of a property/field on this object. This will check if a method called get{$property} exists, then
      * check if a field is available using {@link ViewableData::getField()}, then fall back on a failover object.
-     *
-     * @param string $property
-     * @return mixed
      */
-    public function __get($property)
+    public function __get(string $property): mixed
     {
         // getField() isn't a field-specific getter and shouldn't be treated as such
         $method = "get$property";
@@ -155,11 +126,8 @@ public function __get($property)
     /**
      * Set a property/field on this object. This will check for the existence of a method called set{$property}, then
      * use the {@link ViewableData::setField()} method.
-     *
-     * @param string $property
-     * @param mixed $value
      */
-    public function __set($property, $value)
+    public function __set(string $property, mixed $value): void
     {
         $this->objCacheClear();
         $method = "set$property";
@@ -173,10 +141,8 @@ public function __set($property, $value)
 
     /**
      * Set a failover object to attempt to get data from if it is not present on this object.
-     *
-     * @param ViewableData $failover
      */
-    public function setFailover(ViewableData $failover)
+    public function setFailover(ViewableData $failover): void
     {
         // Ensure cached methods from previous failover are removed
         if ($this->failover) {
@@ -189,56 +155,44 @@ public function setFailover(ViewableData $failover)
 
     /**
      * Get the current failover object if set
-     *
-     * @return ViewableData|null
      */
-    public function getFailover()
+    public function getFailover(): ?ViewableData
     {
         return $this->failover;
     }
 
     /**
      * Check if a field exists on this object. This should be overloaded in child classes.
-     *
-     * @param string $field
-     * @return bool
      */
-    public function hasField($field)
+    public function hasField(string $fieldName): bool
     {
-        return property_exists($this, $field) || $this->hasDynamicData($field);
+        return property_exists($this, $fieldName) || $this->hasDynamicData($fieldName);
     }
 
     /**
      * Get the value of a field on this object. This should be overloaded in child classes.
-     *
-     * @param string $field
-     * @return mixed
      */
-    public function getField($field)
+    public function getField(string $fieldName): mixed
     {
-        if ($this->isAccessibleProperty($field)) {
-            return $this->$field;
+        if ($this->isAccessibleProperty($fieldName)) {
+            return $this->$fieldName;
         }
-        return $this->getDynamicData($field);
+        return $this->getDynamicData($fieldName);
     }
 
     /**
      * Set a field on this object. This should be overloaded in child classes.
-     *
-     * @param string $field
-     * @param mixed $value
-     * @return $this
      */
-    public function setField($field, $value)
+    public function setField(string $fieldName, mixed $value): static
     {
         $this->objCacheClear();
         // prior to PHP 8.2 support ViewableData::setField() simply used `$this->field = $value;`
         // so the following logic essentially mimics this behaviour, though without the use
         // of now deprecated dynamic properties
-        if ($this->isAccessibleProperty($field)) {
-            $this->$field = $value;
+        if ($this->isAccessibleProperty($fieldName)) {
+            $this->$fieldName = $value;
         }
-        return $this->setDynamicData($field, $value);
+        return $this->setDynamicData($fieldName, $value);
     }
 
     public function getDynamicData(string $field): mixed
@@ -322,11 +276,8 @@ public function defineMethods()
      * with references to both this and the new custom data.
      *
      * Note that any fields you specify will take precedence over the fields on this object.
-     *
-     * @param array|ViewableData $data
-     * @return ViewableData_Customised
      */
-    public function customise($data)
+    public function customise(array|ViewableData $data): ViewableData
     {
         if (is_array($data) && (empty($data) || ArrayLib::is_associative($data))) {
             $data = new ArrayData($data);
@@ -346,33 +297,25 @@ public function customise($data)
      *
      * This method should be overridden in subclasses to provide more context about the classes state. For example, a
      * {@link DataObject} class could return false when it is deleted from the database
-     *
-     * @return bool
      */
-    public function exists()
+    public function exists(): bool
     {
         return true;
     }
 
     /**
-     * @return string the class name
+     * Return the class name (though subclasses may return something else)
      */
-    public function __toString()
+    public function __toString(): string
     {
         return static::class;
     }
 
-    /**
-     * @return ViewableData
-     */
-    public function getCustomisedObj()
+    public function getCustomisedObj(): ?ViewableData
     {
         return $this->customisedObject;
     }
 
-    /**
-     * @param ViewableData $object
-     */
     public function setCustomisedObj(ViewableData $object)
     {
         $this->customisedObject = $object;
@@ -384,12 +327,11 @@ public function setCustomisedObj(ViewableData $object)
      * Return the "casting helper" (a piece of PHP code that when evaluated creates a casted value object)
      * for a field on this object. This helper will be a subclass of DBField.
      *
-     * @param string $field
      * @param bool $useFallback If true, fall back on the default casting helper if there isn't an explicit one.
      * @return string|null Casting helper As a constructor pattern, and may include arguments.
      * @throws Exception
      */
-    public function castingHelper($field, bool $useFallback = true)
+    public function castingHelper(string $field, bool $useFallback = true): ?string
     {
         // Get casting if it has been configured.
         // DB fields and PHP methods are all case insensitive so we normalise casing before checking.
@@ -441,11 +383,8 @@ protected function defaultCastingHelper(string $field): string
 
     /**
      * Get the class name a field on this object will be casted to.
-     *
-     * @param string $field
-     * @return string
      */
-    public function castingClass($field)
+    public function castingClass(string $field): string
     {
         // Strip arguments
         $spec = $this->castingHelper($field);
@@ -455,10 +394,9 @@ public function castingClass($field)
     /**
      * Return the string-format type for the given field.
      *
-     * @param string $field
      * @return string 'xml'|'raw'
      */
-    public function escapeTypeForField($field)
+    public function escapeTypeForField(string $field): string
     {
         $class = $this->castingClass($field) ?: $this->config()->get('default_cast');
 
@@ -477,10 +415,9 @@ public function escapeTypeForField($field)
      *  - an SSViewer instance
      *
      * @param string|array|SSViewer $template the template to render into
-     * @param array $customFields fields to customise() the object with before rendering
-     * @return DBHTMLText
+     * @param ViewableData|array|null $customFields fields to customise() the object with before rendering
      */
-    public function renderWith($template, $customFields = null)
+    public function renderWith($template, ViewableData|array|null $customFields = null): DBHTMLText
     {
         if (!is_object($template)) {
             $template = SSViewer::create($template);
@@ -556,14 +493,14 @@ protected function objCacheClear()
      * Get the value of a field on this object, automatically inserting the value into any available casting objects
      * that have been specified.
      *
-     * @param string $fieldName
-     * @param array $arguments
-     * @param bool $cache Cache this object
-     * @param string $cacheName a custom cache name
      * @return object|DBField
      */
-    public function obj($fieldName, $arguments = [], $cache = false, $cacheName = null)
-    {
+    public function obj(
+        string $fieldName,
+        array $arguments = [],
+        bool $cache = false,
+        ?string $cacheName = null
+    ): object {
         if (!$cacheName && $cache) {
             $cacheName = $this->objCacheName($fieldName, $arguments);
         }
@@ -617,26 +554,18 @@ public function obj($fieldName, $arguments = [], $cache = false, $cacheName = nu
      * A simple wrapper around {@link ViewableData::obj()} that automatically caches the result so it can be used again
      * without re-running the method.
      *
-     * @param string $fieldName
-     * @param array $arguments
-     * @param string $identifier an optional custom cache identifier
      * @return Object|DBField
      */
-    public function cachedCall($fieldName, $arguments = [], $identifier = null)
+    public function cachedCall(string $fieldName, array $arguments = [], ?string $cacheName = null): object
     {
-        return $this->obj($fieldName, $arguments, true, $identifier);
+        return $this->obj($fieldName, $arguments, true, $cacheName);
     }
 
     /**
      * Checks if a given method/field has a valid value. If the result is an object, this will return the result of the
      * exists method, otherwise will check if the result is not just an empty paragraph tag.
-     *
-     * @param string $field
-     * @param array $arguments
-     * @param bool $cache
-     * @return bool
      */
-    public function hasValue($field, $arguments = [], $cache = true)
+    public function hasValue(string $field, array $arguments = [], bool $cache = true): bool
     {
         $result = $this->obj($field, $arguments, $cache);
         if ($result instanceof ViewableData) {
@@ -648,13 +577,8 @@ public function hasValue($field, $arguments = [], $cache = true)
     /**
      * Get the string value of a field on this object that has been suitable escaped to be inserted directly into a
      * template.
-     *
-     * @param string $field
-     * @param array $arguments
-     * @param bool $cache
-     * @return string
      */
-    public function XML_val($field, $arguments = [], $cache = false)
+    public function XML_val(string $field, array $arguments = [], bool $cache = false): string
     {
         $result = $this->obj($field, $arguments, $cache);
         // Might contain additional formatting over ->XML(). E.g. parse shortcodes, nl2br()
@@ -665,9 +589,8 @@ public function XML_val($field, $arguments = [], $cache = false)
      * Get an array of XML-escaped values by field name
      *
      * @param array $fields an array of field names
-     * @return array
      */
-    public function getXMLValues($fields)
+    public function getXMLValues(array $fields): array
     {
         $result = [];
 
@@ -678,33 +601,12 @@ public function getXMLValues($fields)
         return $result;
     }
 
-    // ITERATOR SUPPORT ------------------------------------------------------------------------------------------------
-
-    /**
-     * Return a single-item iterator so you can iterate over the fields of a single record.
-     *
-     * This is useful so you can use a single record inside a <% control %> block in a template - and then use
-     * to access individual fields on this object.
-     *
-     * @deprecated 5.2.0 Will be removed without equivalent functionality
-     *
-     * @return ArrayIterator
-     */
-    public function getIterator(): Traversable
-    {
-        Deprecation::notice('5.2.0', 'Will be removed without equivalent functionality');
-        return new ArrayIterator([$this]);
-    }
-
     // UTILITY METHODS -------------------------------------------------------------------------------------------------
 
     /**
      * Find appropriate templates for SSViewer to use to render this object
-     *
-     * @param string $suffix
-     * @return array
      */
-    public function getViewerTemplates($suffix = '')
+    public function getViewerTemplates(string $suffix = ''): array
     {
         return SSViewer::get_templates_by_class(static::class, $suffix, ViewableData::class);
     }
@@ -725,10 +627,9 @@ public function Me(): static
      * stop point - e.g. "Page DataObject ViewableData".
      *
      * @param string $stopAtClass the class to stop at (default: ViewableData)
-     * @return string
      * @uses ClassInfo
      */
-    public function CSSClasses($stopAtClass = ViewableData::class)
+    public function CSSClasses(string $stopAtClass = ViewableData::class): string
     {
         $classes       = [];
         $classAncestry = array_reverse(ClassInfo::ancestry(static::class) ?? []);
@@ -754,11 +655,9 @@ public function CSSClasses($stopAtClass = ViewableData::class)
 
     /**
      * Return debug information about this object that can be rendered into a template
-     *
-     * @return ViewableData_Debugger
      */
-    public function Debug()
+    public function Debug(): ViewableData|string
     {
-        return new ViewableData_Debugger($this);
+        return ViewableData_Debugger::create($this);
     }
 }
diff --git a/src/View/ViewableData_Customised.php b/src/View/ViewableData_Customised.php
index a8589bb51a8..90768c6e4a6 100644
--- a/src/View/ViewableData_Customised.php
+++ b/src/View/ViewableData_Customised.php
@@ -4,17 +4,12 @@
 
 class ViewableData_Customised extends ViewableData
 {
+    protected ViewableData $original;
 
-    /**
-     * @var ViewableData
-     */
-    protected $original, $customised;
+    protected ViewableData $customised;
 
     /**
      * Instantiate a new customised ViewableData object
-     *
-     * @param ViewableData $originalObject
-     * @param ViewableData $customisedObject
      */
     public function __construct(ViewableData $originalObject, ViewableData $customisedObject)
     {
@@ -35,7 +30,7 @@ public function __call($method, $arguments)
         return call_user_func_array([$this->original, $method], $arguments ?? []);
     }
 
-    public function __get($property)
+    public function __get(string $property): mixed
     {
         if (isset($this->customised->$property)) {
             return $this->customised->$property;
@@ -44,12 +39,12 @@ public function __get($property)
         return $this->original->$property;
     }
 
-    public function __set($property, $value)
+    public function __set(string $property, mixed $value): void
     {
         $this->customised->$property = $this->original->$property = $value;
     }
 
-    public function __isset($property)
+    public function __isset(string $property): bool
     {
         return isset($this->customised->$property) || isset($this->original->$property) || parent::__isset($property);
     }
@@ -59,16 +54,20 @@ public function hasMethod($method)
         return $this->customised->hasMethod($method) || $this->original->hasMethod($method);
     }
 
-    public function cachedCall($fieldName, $arguments = null, $identifier = null)
+    public function cachedCall(string $fieldName, array $arguments = [], ?string $cacheName = null): object
     {
         if ($this->customisedHas($fieldName)) {
-            return $this->customised->cachedCall($fieldName, $arguments, $identifier);
+            return $this->customised->cachedCall($fieldName, $arguments, $cacheName);
         }
-        return $this->original->cachedCall($fieldName, $arguments, $identifier);
+        return $this->original->cachedCall($fieldName, $arguments, $cacheName);
     }
 
-    public function obj($fieldName, $arguments = null, $cache = false, $cacheName = null)
-    {
+    public function obj(
+        string $fieldName,
+        array $arguments = [],
+        bool $cache = false,
+        ?string $cacheName = null
+    ): object {
         if ($this->customisedHas($fieldName)) {
             return $this->customised->obj($fieldName, $arguments, $cache, $cacheName);
         }
diff --git a/src/View/ViewableData_Debugger.php b/src/View/ViewableData_Debugger.php
index 9c496c99a4f..2c496610435 100644
--- a/src/View/ViewableData_Debugger.php
+++ b/src/View/ViewableData_Debugger.php
@@ -9,15 +9,8 @@
  */
 class ViewableData_Debugger extends ViewableData
 {
+    protected ViewableData $object;
 
-    /**
-     * @var ViewableData
-     */
-    protected $object;
-
-    /**
-     * @param ViewableData $object
-     */
     public function __construct(ViewableData $object)
     {
         $this->object = $object;
@@ -25,9 +18,9 @@ public function __construct(ViewableData $object)
     }
 
     /**
-     * @return string The rendered debugger
+     * Returns the rendered debugger
      */
-    public function __toString()
+    public function __toString(): string
     {
         return (string)$this->forTemplate();
     }
@@ -35,11 +28,8 @@ public function __toString()
     /**
      * Return debugging information, as XHTML. If a field name is passed, it will show debugging information on that
      * field, otherwise it will show information on all methods and fields.
-     *
-     * @param string $field the field name
-     * @return string
      */
-    public function forTemplate($field = null)
+    public function forTemplate(?string $field = null): string
     {
         // debugging info for a specific field
         $class = get_class($this->object);
diff --git a/tests/php/Core/ClassInfoTest.php b/tests/php/Core/ClassInfoTest.php
index 7528c3f3661..140aa78aa80 100644
--- a/tests/php/Core/ClassInfoTest.php
+++ b/tests/php/Core/ClassInfoTest.php
@@ -152,7 +152,7 @@ public function testAncestry()
     {
         $ancestry = ClassInfo::ancestry(ChildClass::class);
         $expect = [
-            'silverstripe\\view\\viewabledata' => ViewableData::class,
+            'silverstripe\\view\\viewableData' => ViewableData::class,
             'silverstripe\\orm\\dataobject' => DataObject::class,
             'silverstripe\\core\tests\classinfotest\\baseclass' => BaseClass::class,
             'silverstripe\\core\tests\classinfotest\\childclass' => ChildClass::class,
diff --git a/tests/php/Dev/ViewableDataContainsTest/TestObject.php b/tests/php/Dev/ViewableDataContainsTest/TestObject.php
index 0db24f3e400..658cae21040 100644
--- a/tests/php/Dev/ViewableDataContainsTest/TestObject.php
+++ b/tests/php/Dev/ViewableDataContainsTest/TestObject.php
@@ -14,14 +14,14 @@ public function __construct($data)
         $this->data = $data;
     }
 
-    public function hasField($name)
+    public function hasField(string $fieldName): bool
     {
-        return isset($this->data[$name]);
+        return isset($this->data[$fieldName]);
     }
 
-    public function getField($name)
+    public function getField(string $fieldName): mixed
     {
-        return isset($this->data[$name]) ?: null;
+        return isset($this->data[$fieldName]) ?: null;
     }
 
     public function getSomething()
diff --git a/tests/php/ORM/DBFieldTest/TestDataObject.php b/tests/php/ORM/DBFieldTest/TestDataObject.php
index 4d2efd37454..040571abf5d 100644
--- a/tests/php/ORM/DBFieldTest/TestDataObject.php
+++ b/tests/php/ORM/DBFieldTest/TestDataObject.php
@@ -16,10 +16,10 @@ class TestDataObject extends DataObject implements TestOnly
 
     public $setFieldCalledCount = 0;
 
-    public function setField($fieldName, $val)
+    public function setField(string $fieldName, mixed $value): static
     {
         $this->setFieldCalledCount++;
-        return parent::setField($fieldName, $val);
+        return parent::setField($fieldName, $value);
     }
 
     public function setMyTestField($val)
diff --git a/tests/php/ORM/DBFieldTest/TestDbField.php b/tests/php/ORM/DBFieldTest/TestDbField.php
index deb9773c419..264b3b6c8c4 100644
--- a/tests/php/ORM/DBFieldTest/TestDbField.php
+++ b/tests/php/ORM/DBFieldTest/TestDbField.php
@@ -9,7 +9,7 @@
 
 class TestDbField extends DBField implements TestOnly
 {
-    public function requireField()
+    public function requireField(): void
     {
         // Basically the same as DBVarchar but we don't want to test with DBVarchar in case something
         // changes in that class eventually.
diff --git a/tests/php/ORM/DBStringTest/MyStringField.php b/tests/php/ORM/DBStringTest/MyStringField.php
index 9c9afe2376a..c4c8ade800f 100644
--- a/tests/php/ORM/DBStringTest/MyStringField.php
+++ b/tests/php/ORM/DBStringTest/MyStringField.php
@@ -7,7 +7,7 @@
 
 class MyStringField extends DBString implements TestOnly
 {
-    public function requireField()
+    public function requireField(): void
     {
     }
 }
diff --git a/tests/php/ORM/DataObjectTest/MockDynamicAssignmentDBField.php b/tests/php/ORM/DataObjectTest/MockDynamicAssignmentDBField.php
index c2a297fb0c3..6b9d04bfa3b 100644
--- a/tests/php/ORM/DataObjectTest/MockDynamicAssignmentDBField.php
+++ b/tests/php/ORM/DataObjectTest/MockDynamicAssignmentDBField.php
@@ -34,10 +34,8 @@ public function __construct($name = '', $scalarOnly = false, $dynamicAssignment
 
     /**
      * If the field value and $dynamicAssignment are true, we'll try to do a dynamic assignment.
-     * @param $value
-     * @return array|int
      */
-    public function prepValueForDB($value)
+    public function prepValueForDB(mixed $value): array|int|null
     {
         if ($value) {
             return $this->dynamicAssignment
@@ -48,7 +46,7 @@ public function prepValueForDB($value)
         return 0;
     }
 
-    public function scalarValueOnly()
+    public function scalarValueOnly(): bool
     {
         return $this->scalarOnly;
     }
diff --git a/tests/php/View/ArrayDataTest.php b/tests/php/View/ArrayDataTest.php
index 49d93309e43..4ac60b1306a 100644
--- a/tests/php/View/ArrayDataTest.php
+++ b/tests/php/View/ArrayDataTest.php
@@ -11,7 +11,7 @@
 class ArrayDataTest extends SapphireTest
 {
 
-    public function testViewabledataItemsInsideArraydataArePreserved()
+    public function testViewableDataItemsInsideArraydataArePreserved()
     {
         /* ViewableData objects will be preserved, but other objects will be converted */
         $arrayData = new ArrayData(
diff --git a/tests/php/View/SSViewerTest/TestFixture.php b/tests/php/View/SSViewerTest/TestFixture.php
index 7f1da4b265c..c223c5cdd6d 100644
--- a/tests/php/View/SSViewerTest/TestFixture.php
+++ b/tests/php/View/SSViewerTest/TestFixture.php
@@ -18,7 +18,6 @@ public function __construct($name = null)
         parent::__construct();
     }
 
-
     private function argedName($fieldName, $arguments)
     {
         $childName = $this->name ? "$this->name.$fieldName" : $fieldName;
@@ -29,8 +28,12 @@ private function argedName($fieldName, $arguments)
         }
     }
 
-    public function obj($fieldName, $arguments = null, $cache = false, $cacheName = null)
-    {
+    public function obj(
+        string $fieldName,
+        array $arguments = [],
+        bool $cache = false,
+        ?string $cacheName = null
+    ): object {
         $childName = $this->argedName($fieldName, $arguments);
 
         // Special field name Loop### to create a list
@@ -49,8 +52,7 @@ public function obj($fieldName, $arguments = null, $cache = false, $cacheName =
         }
     }
 
-
-    public function XML_val($fieldName, $arguments = null, $cache = false)
+    public function XML_val(string $fieldName, array $arguments = [], bool $cache = false): string
     {
         if (preg_match('/NotSet/i', $fieldName ?? '')) {
             return '';
@@ -63,7 +65,7 @@ public function XML_val($fieldName, $arguments = null, $cache = false)
         }
     }
 
-    public function hasValue($fieldName, $arguments = null, $cache = true)
+    public function hasValue(string $fieldName, array $arguments = [], bool $cache = true): bool
     {
         return (bool)$this->XML_val($fieldName, $arguments);
     }
diff --git a/tests/php/View/ViewableDataTest/Castable.php b/tests/php/View/ViewableDataTest/Castable.php
index e76966f18aa..97c23682372 100644
--- a/tests/php/View/ViewableDataTest/Castable.php
+++ b/tests/php/View/ViewableDataTest/Castable.php
@@ -50,7 +50,7 @@ public function castedUnsafeXML()
         return $this->unsafeXML();
     }
 
-    public function forTemplate()
+    public function forTemplate(): string
     {
         return 'castable';
     }
diff --git a/tests/php/View/ViewableDataTest/Caster.php b/tests/php/View/ViewableDataTest/Caster.php
index 6c68465a516..8b9042e7f65 100644
--- a/tests/php/View/ViewableDataTest/Caster.php
+++ b/tests/php/View/ViewableDataTest/Caster.php
@@ -8,7 +8,7 @@
 class Caster extends ViewableData implements TestOnly
 {
 
-    public function forTemplate()
+    public function forTemplate(): string
     {
         return 'casted';
     }
diff --git a/tests/php/View/ViewableDataTest/RequiresCasting.php b/tests/php/View/ViewableDataTest/RequiresCasting.php
index f66d832a9f9..155acf61cff 100644
--- a/tests/php/View/ViewableDataTest/RequiresCasting.php
+++ b/tests/php/View/ViewableDataTest/RequiresCasting.php
@@ -10,7 +10,7 @@ class RequiresCasting extends ViewableData implements TestOnly
 
     public $test = 'overwritten';
 
-    public function forTemplate()
+    public function forTemplate(): string
     {
         return 'casted';
     }
diff --git a/tests/php/View/ViewableDataTest/UnescapedCaster.php b/tests/php/View/ViewableDataTest/UnescapedCaster.php
index 688cb3077ff..d1eb0735e08 100644
--- a/tests/php/View/ViewableDataTest/UnescapedCaster.php
+++ b/tests/php/View/ViewableDataTest/UnescapedCaster.php
@@ -15,7 +15,7 @@ public function setValue($value)
         $this->value = $value;
     }
 
-    public function forTemplate()
+    public function forTemplate(): string
     {
         return Convert::raw2xml($this->value);
     }