diff --git a/app/controllers/studip_controller.php b/app/controllers/studip_controller.php
index 1b351eafa1da5d340741cac9215b169958aeab2e..3acd6eed440d83f1b34e467a2be3009cc98a8f37 100644
--- a/app/controllers/studip_controller.php
+++ b/app/controllers/studip_controller.php
@@ -22,6 +22,7 @@ abstract class StudipController extends Trails_Controller
     protected $with_session = false; //do we need to have a session for this controller
     protected $allow_nobody = true; //should 'nobody' allowed for this controller or redirected to login?
     protected $_autobind = false;
+    protected $reflection_cache = [];
 
     /**
      * @return false|void
@@ -165,85 +166,12 @@ abstract class StudipController extends Trails_Controller
      */
     public function validate_args(&$args, $types = null)
     {
-        $class_infos = [];
-
-        if ($types === null) {
-            $types = array_fill(0, count($args), 'option');
-        }
-
-        if ($this->has_action($this->current_action)) {
-            $reflection = new ReflectionMethod($this, $this->current_action . '_action');
-            $parameters = $reflection->getParameters();
-            foreach ($parameters as $i => $parameter) {
-                $class_type = $parameter->getType();
-
-                if (!$class_type
-                    || !class_exists($class_type->getName())
-                    || !is_a($class_type->getName(), SimpleORMap::class, true))
-                {
-                    continue;
-                }
-
-                $types[$i] = 'sorm';
-                $class_infos[$i] = [
-                    'model'    => $class_type->getName(),
-                    'var'      => $parameter->getName(),
-                    'optional' => $parameter->isOptional(),
-                ];
-
-                if ($parameter->isOptional() && !isset($args[$i])) {
-                    $args[$i] = $parameter->getDefaultValue();
-                }
-            }
+        if (!is_array($types)) {
+            $types = (array) $types;
         }
 
         foreach ($args as $i => &$arg) {
-            $type = $types[$i] ?: 'option';
-            switch ($type) {
-                case 'int':
-                    $arg = (int) $arg;
-                    break;
-
-                case 'float':
-                    $arg = (float) strtr($arg, ',', '.');
-                    break;
-
-                case 'option':
-                    if (preg_match('/[^\\w,-]/', $arg)) {
-                        throw new Trails_Exception(400);
-                    }
-                    break;
-
-                case 'sorm':
-                    $info = $class_infos[$i];
-
-                    $id = null;
-                    if ($arg != -1) {
-                        $id = $arg;
-                    }
-                    if (mb_strpos($id, SimpleORMap::ID_SEPARATOR) !== false) {
-                        $id = explode(SimpleORMap::ID_SEPARATOR, $id);
-                    }
-
-                    $reflection = new ReflectionClass($info['model']);
-
-                    $sorm = $reflection->newInstance($id);
-                    if (!$info['optional'] && $sorm->isNew()) {
-                        throw new Trails_Exception(
-                            404,
-                            "Parameter {$info['var']} could not be resolved with value {$arg}"
-                        );
-                    }
-
-                    $arg = $sorm;
-                    if ($this->_autobind) {
-                        $this->{$info['var']} = $arg;
-                    }
-                    break;
-
-                default:
-                    throw new Trails_Exception(500, 'Unknown type "' . $type . '"');
-            }
+            $arg = $this->convertArgumentFromURL($arg, $i, $types[$i] ?? $this->getTypeForActionParameter($i));
         }
 
         reset($args);
@@ -289,7 +217,7 @@ abstract class StudipController extends Trails_Controller
 
         // Extract fragment (if any)
         if (strpos($to, '#') !== false) {
-            list($args[0], $fragment) = explode('#', $to);
+            [$args[0], $fragment] = explode('#', $to);
         }
 
         // Extract parameters (if any)
@@ -299,12 +227,10 @@ abstract class StudipController extends Trails_Controller
         }
 
         // Map any sorm objects to their ids
-        $args = array_map(function ($arg) {
-            if (is_object($arg) && $arg instanceof SimpleORMap) {
-                return $arg->isNew() ? -1 : $arg->id;
-            }
-            return $arg;
-        }, $args);
+        $args = array_map(
+            [$this, 'convertArgumentForURL'],
+            $args
+        );
 
         $url = parent::url_for(...$args);
 
@@ -810,4 +736,146 @@ abstract class StudipController extends Trails_Controller
 
         return $body_id;
     }
+
+    /**
+     * Converts an argument passed to url_for()/link_for() for usage in an url.
+     *
+     * @param mixed $argument
+     * @return mixed
+     */
+    protected function convertArgumentForURL($argument)
+    {
+        if (is_object($argument) && $argument instanceof SimpleORMap) {
+            return $argument->isNew() ? -1 : $argument->id;
+        }
+        return $argument;
+    }
+
+    /**
+     * @param mixed  $argument
+     * @param int    $index
+     * @param string $type
+     *
+     * @return mixed
+     */
+    protected function convertArgumentFromURL($argument, int $index, string $type)
+    {
+        if ($type === 'int') {
+            return (int) $argument;
+        }
+
+        if ($type === 'float') {
+            return (float) strtr($argument, ',', '.');
+        }
+
+        if ($type === 'sorm') {
+            $parameter = $this->getReflectedParameter($index);
+            if ($parameter->isOptional() && !isset($argument)) {
+                return $parameter->getDefaultValue();
+            }
+
+            $sorm = $this->loadSORMParameter($parameter->getType()->getName(), $argument);
+
+            if ($sorm->isNew() && !$parameter->isOptional()) {
+                throw new Trails_Exception(
+                    404,
+                    "Parameter {$parameter->getName()} could not be resolved with value {$argument}"
+                );
+            }
+
+            if ($this->_autobind) {
+                $this->{$parameter->getName()} = $sorm;
+            }
+
+            return $sorm;
+        }
+
+        if ($type !== 'option') {
+            throw new Trails_Exception(500, 'Unknown type "' . $type . '"');
+        }
+
+        if (preg_match('/[^\\w,-]/', $argument)) {
+            throw new Trails_Exception(400);
+        }
+
+        return $argument;
+    }
+
+    /**
+     * @param int $index
+     *
+     * @return ReflectionMethod
+     * @throws ReflectionException
+     */
+    protected function getTypeForActionParameter(int $index, string $action = null): string
+    {
+        $parameter = $this->getReflectedParameter($index, $action);
+        if (!$parameter) {
+            return 'option';
+        }
+
+        $type = $parameter->getType();
+        if (!$type) {
+            return 'option';
+        }
+
+        $type_name = $type->getName();
+
+        if (in_array($type_name, ['int', 'float'])) {
+            return $type_name;
+        }
+
+        if (class_exists($type_name) && is_a($type_name, SimpleORMap::class, true)) {
+            return 'sorm';
+        }
+
+        return 'option';
+    }
+
+    /**
+     * @param int         $index
+     * @param string|null $action
+     *
+     * @return ReflectionParameter|null
+     * @throws ReflectionException
+     */
+    protected function getReflectedParameter(int $index, string $action = null): ?ReflectionParameter
+    {
+        if ($action === null) {
+            $action = $this->current_action;
+        }
+
+        if (!$this->has_action($action)) {
+            return null;
+        }
+
+        if (!isset($this->reflection_cache[$action])) {
+            $reflection = new ReflectionMethod(
+                $this,
+                "{$action}_action"
+            );
+            $this->reflection_cache[$action] = $reflection->getParameters();
+        }
+
+        return $this->reflection_cache[$action][$index] ?? null;
+    }
+
+    /**
+     * @param string $class_name
+     * @param mixed  $argument
+     *
+     * @return SimpleORMap
+     * @throws ReflectionException
+     */
+    protected function loadSORMParameter(string $class_name, $argument): SimpleORMap
+    {
+        $id = $argument != -1 ? $argument : null;
+        if (mb_strpos($id, SimpleORMap::ID_SEPARATOR) !== false) {
+            $id = explode(SimpleORMap::ID_SEPARATOR, $id);
+        }
+
+        $reflection = new ReflectionClass($class_name);
+
+        return $reflection->newInstance($id);
+    }
 }