system/Router/RouteCollection.php
protected function create(string $verb, string $from, $to, array $options = null)
	{
		$overwrite = false;
		$prefix    = is_null($this->group) ? '' : $this->group . '/';

		$from = filter_var($prefix . $from, FILTER_SANITIZE_STRING);

		// While we want to add a route within a group of '/',
		// it doesn't work with matching, so remove them...
		if ($from !== '/')
		{
			$from = trim($from, '/');
		}

		$options = array_merge((array) $this->currentOptions, (array) $options);

		// Hostname limiting?
		if (! empty($options['hostname']))
		{
			// @todo determine if there's a way to whitelist hosts?
			if (isset($_SERVER['HTTP_HOST']) && strtolower($_SERVER['HTTP_HOST']) !== strtolower($options['hostname']))
			{
				return;
			}

			$overwrite = true;
		}

		// Limiting to subdomains?
		else if (isset($options['subdomain']) && ! empty($options['subdomain']))
		{
			// If we don't match the current subdomain, then
			// we don't need to add the route.
			if (! $this->checkSubdomains($options['subdomain']))
			{
				return;
			}

			$overwrite = true;
		}

		// Are we offsetting the binds?
		// If so, take care of them here in one
		// fell swoop.
		if (isset($options['offset']) && is_string($to))
		{
			// Get a constant string to work with.
			$to = preg_replace('/(\$\d+)/', '$X', $to);

			for ($i = (int) $options['offset'] + 1; $i < (int) $options['offset'] + 7; $i ++)
			{
				$to = preg_replace_callback(
						'/\$X/', function ($m) use ($i) {
							return '$' . $i;
						}, $to, 1
				);
			}
		}

		// Replace our regex pattern placeholders with the actual thing
		// so that the Router doesn't need to know about any of this.
		foreach ($this->placeholders as $tag => $pattern)
		{
			$from = str_ireplace(':' . $tag, $pattern, $from);
		}

		// If no namespace found, add the default namespace
		if (is_string($to) && strpos($to, '\\') === false)
		{
			$namespace = $options['namespace'] ?? $this->defaultNamespace;
			$to        = trim($namespace, '\\') . '\\' . $to;
		}

		// Always ensure that we escape our namespace so we're not pointing to
		// \CodeIgniter\Routes\Controller::method.
		if (is_string($to))
		{
			$to = '\\' . ltrim($to, '\\');
		}

		$name = $options['as'] ?? $from;

		// Don't overwrite any existing 'froms' so that auto-discovered routes
		// do not overwrite any app/Config/Routes settings. The app
		// routes should always be the "source of truth".
		// this works only because discovered routes are added just prior
		// to attempting to route the request.
		if (isset($this->routes[$verb][$name]) && ! $overwrite)
		{
			return;
		}

		$this->routes[$verb][$name] = [
			'route' => [$from => $to],
		];

		$this->routesOptions[$from] = $options;

		// Is this a redirect?
		if (isset($options['redirect']) && is_numeric($options['redirect']))
		{
			$this->routes['*'][$name]['redirect'] = $options['redirect'];
		}
	}
protected function create(string $verb, string $from, $to, array $options = null)
	{
		$overwrite = false;
		$prefix    = is_null($this->group) ? '' : $this->group . '/';

		$from = filter_var($prefix . $from, FILTER_SANITIZE_STRING);

		// While we want to add a route within a group of '/',
		// it doesn't work with matching, so remove them...
		if ($from !== '/')
		{
			$from = trim($from, '/');
		}

		$options = array_merge((array) $this->currentOptions, (array) $options);

		// Hostname limiting?
		if (! empty($options['hostname']))
		{
			// @todo determine if there's a way to whitelist hosts?
			if (isset($_SERVER['HTTP_HOST']) && strtolower($_SERVER['HTTP_HOST']) !== strtolower($options['hostname']))
			{
				return;
			}

			$overwrite = true;
		}

		// Limiting to subdomains?
		else if (isset($options['subdomain']) && ! empty($options['subdomain']))
		{
			// If we don't match the current subdomain, then
			// we don't need to add the route.
			if (! $this->checkSubdomains($options['subdomain']))
			{
				return;
			}

			$overwrite = true;
		}

		// Are we offsetting the binds?
		// If so, take care of them here in one
		// fell swoop.
		if (isset($options['offset']) && is_string($to))
		{
			// Get a constant string to work with.
			$to = preg_replace('/(\$\d+)/', '$X', $to);

			for ($i = (int) $options['offset'] + 1; $i < (int) $options['offset'] + 7; $i ++)
			{
				$to = preg_replace_callback(
						'/\$X/', function ($m) use ($i) {
							return '$' . $i;
						}, $to, 1
				);
			}
		}

		// Replace our regex pattern placeholders with the actual thing
		// so that the Router doesn't need to know about any of this.
		foreach ($this->placeholders as $tag => $pattern)
		{
			$from = str_ireplace(':' . $tag, $pattern, $from);
		}

		// If no namespace found, add the default namespace
		if (is_string($to) && strpos($to, '\\') === false)
		{
			$namespace = $options['namespace'] ?? $this->defaultNamespace;
			$to        = trim($namespace, '\\') . '\\' . $to;
		}

		// Always ensure that we escape our namespace so we're not pointing to
		// \CodeIgniter\Routes\Controller::method.
		if (is_string($to))
		{
			$to = '\\' . ltrim($to, '\\');
		}

		$name = $options['as'] ?? $from;

		// Don't overwrite any existing 'froms' so that auto-discovered routes
		// do not overwrite any app/Config/Routes settings. The app
		// routes should always be the "source of truth".
		// this works only because discovered routes are added just prior
		// to attempting to route the request.
		if (isset($this->routes[$verb][$name]) && ! $overwrite)
		{
			return;
		}

		$this->routes[$verb][$name] = [
			'route' => [$from => $to],
		];

		$this->routesOptions[$from] = $options;

		// Is this a redirect?
		if (isset($options['redirect']) && is_numeric($options['redirect']))
		{
			$this->routes['*'][$name]['redirect'] = $options['redirect'];
		}
	}
Does the heavy lifting of creating an actual route. You must specify
the request method(s) that this route will work for. They can be separated
by a pipe character "|" if there is more than one.

@param string       $verb
@param string       $from
@param string|array $to
@param array        $options