diff --git a/app/Actions/CommentRepo.php b/app/Actions/CommentRepo.php
index 8121dfc5c..8061c4542 100644
--- a/app/Actions/CommentRepo.php
+++ b/app/Actions/CommentRepo.php
@@ -90,8 +90,9 @@ class CommentRepo
      */
     protected function getNextLocalId(Entity $entity): int
     {
-        $comments = $entity->comments(false)->orderBy('local_id', 'desc')->first();
+        /** @var Comment $comment */
+        $comment = $entity->comments(false)->orderBy('local_id', 'desc')->first();
 
-        return ($comments->local_id ?? 0) + 1;
+        return ($comment->local_id ?? 0) + 1;
     }
 }
diff --git a/app/Auth/Access/Guards/LdapSessionGuard.php b/app/Auth/Access/Guards/LdapSessionGuard.php
index 7f6965937..078487224 100644
--- a/app/Auth/Access/Guards/LdapSessionGuard.php
+++ b/app/Auth/Access/Guards/LdapSessionGuard.php
@@ -94,7 +94,7 @@ class LdapSessionGuard extends ExternalBaseSessionGuard
         }
 
         // Attach avatar if non-existent
-        if (is_null($user->avatar)) {
+        if (!$user->avatar()->exists()) {
             $this->ldapService->saveAndAttachAvatar($user, $userDetails);
         }
 
diff --git a/app/Auth/Access/Ldap.php b/app/Auth/Access/Ldap.php
index b5c70e498..6259de0ae 100644
--- a/app/Auth/Access/Ldap.php
+++ b/app/Auth/Access/Ldap.php
@@ -10,14 +10,10 @@ namespace BookStack\Auth\Access;
 class Ldap
 {
     /**
-     * Connect to a LDAP server.
-     *
-     * @param string $hostName
-     * @param int    $port
-     *
+     * Connect to an LDAP server.
      * @return resource
      */
-    public function connect($hostName, $port)
+    public function connect(string $hostName, int $port)
     {
         return ldap_connect($hostName, $port);
     }
@@ -26,12 +22,9 @@ class Ldap
      * Set the value of a LDAP option for the given connection.
      *
      * @param resource $ldapConnection
-     * @param int      $option
      * @param mixed    $value
-     *
-     * @return bool
      */
-    public function setOption($ldapConnection, $option, $value)
+    public function setOption($ldapConnection, int $option, $value): bool
     {
         return ldap_set_option($ldapConnection, $option, $value);
     }
@@ -47,12 +40,9 @@ class Ldap
     /**
      * Set the version number for the given ldap connection.
      *
-     * @param $ldapConnection
-     * @param $version
-     *
-     * @return bool
+     * @param resource $ldapConnection
      */
-    public function setVersion($ldapConnection, $version)
+    public function setVersion($ldapConnection, int $version): bool
     {
         return $this->setOption($ldapConnection, LDAP_OPT_PROTOCOL_VERSION, $version);
     }
diff --git a/app/Console/Commands/ResetMfa.php b/app/Console/Commands/ResetMfa.php
index 031bec04b..9074a4a46 100644
--- a/app/Console/Commands/ResetMfa.php
+++ b/app/Console/Commands/ResetMfa.php
@@ -49,9 +49,10 @@ class ResetMfa extends Command
             return 1;
         }
 
-        /** @var User $user */
         $field = $id ? 'id' : 'email';
         $value = $id ?: $email;
+
+        /** @var User $user */
         $user = User::query()
             ->where($field, '=', $value)
             ->first();
diff --git a/app/Entities/Models/Chapter.php b/app/Entities/Models/Chapter.php
index abf496b44..75630832b 100644
--- a/app/Entities/Models/Chapter.php
+++ b/app/Entities/Models/Chapter.php
@@ -3,13 +3,14 @@
 namespace BookStack\Entities\Models;
 
 use Illuminate\Database\Eloquent\Factories\HasFactory;
+use Illuminate\Database\Eloquent\Relations\HasMany;
 use Illuminate\Support\Collection;
 
 /**
  * Class Chapter.
  *
  * @property Collection<Page> $pages
- * @property mixed description
+ * @property string $description
  */
 class Chapter extends BookChild
 {
@@ -22,12 +23,8 @@ class Chapter extends BookChild
 
     /**
      * Get the pages that this chapter contains.
-     *
-     * @param string $dir
-     *
-     * @return mixed
      */
-    public function pages($dir = 'ASC')
+    public function pages(string $dir = 'ASC'): HasMany
     {
         return $this->hasMany(Page::class)->orderBy('priority', $dir);
     }
@@ -35,7 +32,7 @@ class Chapter extends BookChild
     /**
      * Get the url of this chapter.
      */
-    public function getUrl($path = ''): string
+    public function getUrl(string $path = ''): string
     {
         $parts = [
             'books',
diff --git a/app/Entities/Models/Deletion.php b/app/Entities/Models/Deletion.php
index dab89ce37..3face841b 100644
--- a/app/Entities/Models/Deletion.php
+++ b/app/Entities/Models/Deletion.php
@@ -9,7 +9,7 @@ use Illuminate\Database\Eloquent\Relations\BelongsTo;
 use Illuminate\Database\Eloquent\Relations\MorphTo;
 
 /**
- * @property Model deletable
+ * @property Model $deletable
  */
 class Deletion extends Model implements Loggable
 {
@@ -22,7 +22,7 @@ class Deletion extends Model implements Loggable
     }
 
     /**
-     * The the user that performed the deletion.
+     * Get the user that performed the deletion.
      */
     public function deleter(): BelongsTo
     {
@@ -48,7 +48,11 @@ class Deletion extends Model implements Loggable
     {
         $deletable = $this->deletable()->first();
 
-        return "Deletion ({$this->id}) for {$deletable->getType()} ({$deletable->id}) {$deletable->name}";
+        if ($deletable instanceof Entity) {
+            return "Deletion ({$this->id}) for {$deletable->getType()} ({$deletable->id}) {$deletable->name}";
+        }
+
+        return "Deletion ({$this->id})";
     }
 
     /**
diff --git a/app/Entities/Models/Page.php b/app/Entities/Models/Page.php
index fbe0db41b..27d5dc6a4 100644
--- a/app/Entities/Models/Page.php
+++ b/app/Entities/Models/Page.php
@@ -106,7 +106,7 @@ class Page extends BookChild
     /**
      * Get the url of this page.
      */
-    public function getUrl($path = ''): string
+    public function getUrl(string $path = ''): string
     {
         $parts = [
             'books',
diff --git a/app/Entities/Models/PageRevision.php b/app/Entities/Models/PageRevision.php
index b994e7a04..336fd67da 100644
--- a/app/Entities/Models/PageRevision.php
+++ b/app/Entities/Models/PageRevision.php
@@ -22,6 +22,8 @@ use Illuminate\Database\Eloquent\Relations\BelongsTo;
  * @property string $html
  * @property int    $revision_number
  * @property Page   $page
+ *
+ * @property-read ?User $createdBy
  */
 class PageRevision extends Model
 {
diff --git a/app/Entities/Tools/PageEditActivity.php b/app/Entities/Tools/PageEditActivity.php
index ef6c085ac..9981a6ed7 100644
--- a/app/Entities/Tools/PageEditActivity.php
+++ b/app/Entities/Tools/PageEditActivity.php
@@ -35,7 +35,13 @@ class PageEditActivity
         $pageDraftEdits = $this->activePageEditingQuery(60)->get();
         $count = $pageDraftEdits->count();
 
-        $userMessage = $count > 1 ? trans('entities.pages_draft_edit_active.start_a', ['count' => $count]) : trans('entities.pages_draft_edit_active.start_b', ['userName' => $pageDraftEdits->first()->createdBy->name]);
+        $userMessage = trans('entities.pages_draft_edit_active.start_a', ['count' => $count]);
+        if ($count === 1) {
+            /** @var PageRevision $firstDraft */
+            $firstDraft = $pageDraftEdits->first();
+            $userMessage = trans('entities.pages_draft_edit_active.start_b', ['userName' => $firstDraft->createdBy->name ?? '']);
+        }
+
         $timeMessage = trans('entities.pages_draft_edit_active.time_b', ['minCount'=> 60]);
 
         return trans('entities.pages_draft_edit_active.message', ['start' => $userMessage, 'time' => $timeMessage]);
diff --git a/app/Entities/Tools/SearchRunner.php b/app/Entities/Tools/SearchRunner.php
index df566eb0b..223494d46 100644
--- a/app/Entities/Tools/SearchRunner.php
+++ b/app/Entities/Tools/SearchRunner.php
@@ -47,7 +47,7 @@ class SearchRunner
     /**
      * Search all entities in the system.
      * The provided count is for each entity to search,
-     * Total returned could can be larger and not guaranteed.
+     * Total returned could be larger and not guaranteed.
      */
     public function searchEntities(SearchOptions $searchOpts, string $entityType = 'all', int $page = 1, int $count = 20, string $action = 'view'): array
     {
@@ -68,11 +68,15 @@ class SearchRunner
             if (!in_array($entityType, $entityTypes)) {
                 continue;
             }
+
             $search = $this->searchEntityTable($searchOpts, $entityType, $page, $count, $action);
+            /** @var int $entityTotal */
             $entityTotal = $this->searchEntityTable($searchOpts, $entityType, $page, $count, $action, true);
-            if ($entityTotal > $page * $count) {
+
+            if ($entityTotal > ($page * $count)) {
                 $hasMore = true;
             }
+
             $total += $entityTotal;
             $results = $results->merge($search);
         }
diff --git a/app/Entities/Tools/SiblingFetcher.php b/app/Entities/Tools/SiblingFetcher.php
index e9dad0e13..249e0038e 100644
--- a/app/Entities/Tools/SiblingFetcher.php
+++ b/app/Entities/Tools/SiblingFetcher.php
@@ -5,6 +5,7 @@ namespace BookStack\Entities\Tools;
 use BookStack\Entities\EntityProvider;
 use BookStack\Entities\Models\Book;
 use BookStack\Entities\Models\Bookshelf;
+use BookStack\Entities\Models\Page;
 use Illuminate\Support\Collection;
 
 class SiblingFetcher
@@ -18,18 +19,18 @@ class SiblingFetcher
         $entities = [];
 
         // Page in chapter
-        if ($entity->isA('page') && $entity->chapter) {
+        if ($entity instanceof Page && $entity->chapter) {
             $entities = $entity->chapter->getVisiblePages();
         }
 
         // Page in book or chapter
-        if (($entity->isA('page') && !$entity->chapter) || $entity->isA('chapter')) {
+        if (($entity instanceof Page && !$entity->chapter) || $entity->isA('chapter')) {
             $entities = $entity->book->getDirectChildren();
         }
 
         // Book
         // Gets just the books in a shelf if shelf is in context
-        if ($entity->isA('book')) {
+        if ($entity instanceof Book) {
             $contextShelf = (new ShelfContext())->getContextualShelfForBook($entity);
             if ($contextShelf) {
                 $entities = $contextShelf->visibleBooks()->get();
@@ -38,8 +39,8 @@ class SiblingFetcher
             }
         }
 
-        // Shelve
-        if ($entity->isA('bookshelf')) {
+        // Shelf
+        if ($entity instanceof Bookshelf) {
             $entities = Bookshelf::visible()->get();
         }
 
diff --git a/app/Entities/Tools/SlugGenerator.php b/app/Entities/Tools/SlugGenerator.php
index 52e5700da..e715f769f 100644
--- a/app/Entities/Tools/SlugGenerator.php
+++ b/app/Entities/Tools/SlugGenerator.php
@@ -4,13 +4,14 @@ namespace BookStack\Entities\Tools;
 
 use BookStack\Entities\Models\BookChild;
 use BookStack\Interfaces\Sluggable;
+use BookStack\Model;
 use Illuminate\Support\Str;
 
 class SlugGenerator
 {
     /**
      * Generate a fresh slug for the given entity.
-     * The slug will generated so it does not conflict within the same parent item.
+     * The slug will be generated so that it doesn't conflict within the same parent item.
      */
     public function generate(Sluggable $model): string
     {
@@ -38,6 +39,7 @@ class SlugGenerator
     /**
      * Check if a slug is already in-use for this
      * type of model within the same parent.
+     * @param Sluggable&Model $model
      */
     protected function slugInUse(string $slug, Sluggable $model): bool
     {
diff --git a/app/Http/Controllers/Auth/ConfirmEmailController.php b/app/Http/Controllers/Auth/ConfirmEmailController.php
index c5466aecd..3e7d4a836 100644
--- a/app/Http/Controllers/Auth/ConfirmEmailController.php
+++ b/app/Http/Controllers/Auth/ConfirmEmailController.php
@@ -10,10 +10,7 @@ use BookStack\Exceptions\UserTokenExpiredException;
 use BookStack\Exceptions\UserTokenNotFoundException;
 use BookStack\Http\Controllers\Controller;
 use Exception;
-use Illuminate\Http\RedirectResponse;
 use Illuminate\Http\Request;
-use Illuminate\Routing\Redirector;
-use Illuminate\View\View;
 
 class ConfirmEmailController extends Controller
 {
@@ -57,33 +54,23 @@ class ConfirmEmailController extends Controller
     /**
      * Confirms an email via a token and logs the user into the system.
      *
-     * @param $token
-     *
      * @throws ConfirmationEmailException
      * @throws Exception
-     *
-     * @return RedirectResponse|Redirector
      */
-    public function confirm($token)
+    public function confirm(string $token)
     {
         try {
             $userId = $this->emailConfirmationService->checkTokenAndGetUserId($token);
-        } catch (Exception $exception) {
-            if ($exception instanceof UserTokenNotFoundException) {
-                $this->showErrorNotification(trans('errors.email_confirmation_invalid'));
+        } catch (UserTokenNotFoundException $exception) {
+            $this->showErrorNotification(trans('errors.email_confirmation_invalid'));
 
-                return redirect('/register');
-            }
+            return redirect('/register');
+        } catch (UserTokenExpiredException $exception) {
+            $user = $this->userRepo->getById($exception->userId);
+            $this->emailConfirmationService->sendConfirmation($user);
+            $this->showErrorNotification(trans('errors.email_confirmation_expired'));
 
-            if ($exception instanceof UserTokenExpiredException) {
-                $user = $this->userRepo->getById($exception->userId);
-                $this->emailConfirmationService->sendConfirmation($user);
-                $this->showErrorNotification(trans('errors.email_confirmation_expired'));
-
-                return redirect('/register/confirm');
-            }
-
-            throw $exception;
+            return redirect('/register/confirm');
         }
 
         $user = $this->userRepo->getById($userId);
@@ -99,10 +86,6 @@ class ConfirmEmailController extends Controller
 
     /**
      * Resend the confirmation email.
-     *
-     * @param Request $request
-     *
-     * @return View
      */
     public function resend(Request $request)
     {
diff --git a/app/Http/Controllers/Controller.php b/app/Http/Controllers/Controller.php
index df450d051..047e0bed9 100644
--- a/app/Http/Controllers/Controller.php
+++ b/app/Http/Controllers/Controller.php
@@ -165,7 +165,7 @@ abstract class Controller extends BaseController
     /**
      * Log an activity in the system.
      *
-     * @param string|Loggable
+     * @param $detail string|Loggable
      */
     protected function logActivity(string $type, $detail = ''): void
     {
diff --git a/app/Http/Controllers/FavouriteController.php b/app/Http/Controllers/FavouriteController.php
index c8ba3078c..8643073c9 100644
--- a/app/Http/Controllers/FavouriteController.php
+++ b/app/Http/Controllers/FavouriteController.php
@@ -66,7 +66,7 @@ class FavouriteController extends Controller
      * @throws \Illuminate\Validation\ValidationException
      * @throws \Exception
      */
-    protected function getValidatedModelFromRequest(Request $request): Favouritable
+    protected function getValidatedModelFromRequest(Request $request): Entity
     {
         $modelInfo = $this->validate($request, [
             'type' => ['required', 'string'],
diff --git a/app/Http/Middleware/CheckUserHasPermission.php b/app/Http/Middleware/CheckUserHasPermission.php
index 4a6a06468..b5678e734 100644
--- a/app/Http/Middleware/CheckUserHasPermission.php
+++ b/app/Http/Middleware/CheckUserHasPermission.php
@@ -12,7 +12,7 @@ class CheckUserHasPermission
      *
      * @param \Illuminate\Http\Request $request
      * @param \Closure                 $next
-     * @param                          $permission
+     * @param string                   $permission
      *
      * @return mixed
      */
diff --git a/app/Interfaces/Sluggable.php b/app/Interfaces/Sluggable.php
index 24ee1bab2..2d56e847e 100644
--- a/app/Interfaces/Sluggable.php
+++ b/app/Interfaces/Sluggable.php
@@ -2,18 +2,12 @@
 
 namespace BookStack\Interfaces;
 
-use Illuminate\Database\Eloquent\Builder;
-
 /**
- * Interface Sluggable.
- *
  * Assigned to models that can have slugs.
  * Must have the below properties.
  *
  * @property int    $id
  * @property string $name
- *
- * @method Builder newQuery
  */
 interface Sluggable
 {
diff --git a/app/Model.php b/app/Model.php
index 8520060f4..bd524332c 100644
--- a/app/Model.php
+++ b/app/Model.php
@@ -9,12 +9,9 @@ class Model extends EloquentModel
     /**
      * Provides public access to get the raw attribute value from the model.
      * Used in areas where no mutations are required but performance is critical.
-     *
-     * @param $key
-     *
      * @return mixed
      */
-    public function getRawAttribute($key)
+    public function getRawAttribute(string $key)
     {
         return parent::getAttributeFromArray($key);
     }
diff --git a/app/Traits/HasCreatorAndUpdater.php b/app/Traits/HasCreatorAndUpdater.php
index a48936bc8..7c60be750 100644
--- a/app/Traits/HasCreatorAndUpdater.php
+++ b/app/Traits/HasCreatorAndUpdater.php
@@ -6,8 +6,8 @@ use BookStack\Auth\User;
 use Illuminate\Database\Eloquent\Relations\BelongsTo;
 
 /**
- * @property int created_by
- * @property int updated_by
+ * @property int $created_by
+ * @property int $updated_by
  */
 trait HasCreatorAndUpdater
 {
diff --git a/app/Traits/HasOwner.php b/app/Traits/HasOwner.php
index 6700ff4df..c0fefafa9 100644
--- a/app/Traits/HasOwner.php
+++ b/app/Traits/HasOwner.php
@@ -6,7 +6,7 @@ use BookStack\Auth\User;
 use Illuminate\Database\Eloquent\Relations\BelongsTo;
 
 /**
- * @property int owned_by
+ * @property int $owned_by
  */
 trait HasOwner
 {
diff --git a/app/Uploads/ImageRepo.php b/app/Uploads/ImageRepo.php
index 2a4ea424f..494ff3ac0 100644
--- a/app/Uploads/ImageRepo.php
+++ b/app/Uploads/ImageRepo.php
@@ -236,7 +236,7 @@ class ImageRepo
             ->get(['id', 'name', 'slug', 'book_id']);
 
         foreach ($pages as $page) {
-            $page->url = $page->getUrl();
+            $page->setAttribute('url', $page->getUrl());
         }
 
         return $pages->all();
diff --git a/app/Uploads/ImageService.php b/app/Uploads/ImageService.php
index 6d4902589..b8477eb40 100644
--- a/app/Uploads/ImageService.php
+++ b/app/Uploads/ImageService.php
@@ -8,6 +8,7 @@ use Exception;
 use Illuminate\Contracts\Cache\Repository as Cache;
 use Illuminate\Contracts\Filesystem\FileNotFoundException;
 use Illuminate\Contracts\Filesystem\Filesystem as Storage;
+use Illuminate\Filesystem\FilesystemAdapter;
 use Illuminate\Filesystem\FilesystemManager;
 use Illuminate\Support\Facades\DB;
 use Illuminate\Support\Facades\Log;
@@ -436,10 +437,12 @@ class ImageService
      */
     public function pathExistsInLocalSecure(string $imagePath): bool
     {
+        /** @var FilesystemAdapter $disk */
         $disk = $this->getStorageDisk('gallery');
 
         // Check local_secure is active
         return $this->usingSecureImages()
+            && $disk instanceof FilesystemAdapter
             // Check the image file exists
             && $disk->exists($imagePath)
             // Check the file is likely an image file
diff --git a/app/Util/HtmlContentFilter.php b/app/Util/HtmlContentFilter.php
index 1943aa780..08dde7048 100644
--- a/app/Util/HtmlContentFilter.php
+++ b/app/Util/HtmlContentFilter.php
@@ -4,6 +4,7 @@ namespace BookStack\Util;
 
 use DOMAttr;
 use DOMDocument;
+use DOMElement;
 use DOMNodeList;
 use DOMXPath;
 
@@ -92,7 +93,9 @@ class HtmlContentFilter
         /** @var DOMAttr $attr */
         foreach ($attrs as $attr) {
             $attrName = $attr->nodeName;
-            $attr->parentNode->removeAttribute($attrName);
+            /** @var DOMElement $parentNode */
+            $parentNode = $attr->parentNode;
+            $parentNode->removeAttribute($attrName);
         }
     }
 }
diff --git a/phpstan.neon.dist b/phpstan.neon.dist
index 815b1c187..f3aa47e12 100644
--- a/phpstan.neon.dist
+++ b/phpstan.neon.dist
@@ -15,7 +15,7 @@ parameters:
       - bootstrap/phpstan.php
 
     ignoreErrors:
-#        - '#Unsafe usage of new static#'
+    #  - '#PHPDoc tag @throws with type .*?Psr\\SimpleCache\\InvalidArgumentException.*? is not subtype of Throwable#'
 
     excludePaths:
         - ./Config/**/*.php