diff --git a/phpstan-baseline.php b/phpstan-baseline.php
index 1bec5a750..abd5d1957 100644
--- a/phpstan-baseline.php
+++ b/phpstan-baseline.php
@@ -361,18 +361,6 @@
'count' => 1,
'path' => __DIR__ . '/src/Cache/SelectOptionsCacher.php',
];
-$ignoreErrors[] = [
- 'message' => '#^Cannot access offset \'host\' on array\\{scheme\\?\\: string, host\\?\\: string, port\\?\\: int\\<0, 65535\\>, user\\?\\: string, pass\\?\\: string, path\\?\\: string, query\\?\\: string, fragment\\?\\: string\\}\\|false\\.$#',
- 'identifier' => 'offsetAccess.nonOffsetAccessible',
- 'count' => 2,
- 'path' => __DIR__ . '/src/Canonical.php',
-];
-$ignoreErrors[] = [
- 'message' => '#^Cannot access offset \'scheme\' on array\\{scheme\\?\\: string, host\\?\\: string, port\\?\\: int\\<0, 65535\\>, user\\?\\: string, pass\\?\\: string, path\\?\\: string, query\\?\\: string, fragment\\?\\: string\\}\\|false\\.$#',
- 'identifier' => 'offsetAccess.nonOffsetAccessible',
- 'count' => 3,
- 'path' => __DIR__ . '/src/Canonical.php',
-];
$ignoreErrors[] = [
'message' => '#^Method Bolt\\\\Canonical\\:\\:generateLink\\(\\) has parameter \\$canonical with no type specified\\.$#',
'identifier' => 'missingType.parameter',
@@ -1357,12 +1345,6 @@
'count' => 1,
'path' => __DIR__ . '/src/Controller/Backend/ResetPasswordController.php',
];
-$ignoreErrors[] = [
- 'message' => '#^Method Bolt\\\\Controller\\\\Backend\\\\ResetPasswordController\\:\\:buildResetEmail\\(\\) has parameter \\$config with no value type specified in iterable type array\\.$#',
- 'identifier' => 'missingType.iterableValue',
- 'count' => 1,
- 'path' => __DIR__ . '/src/Controller/Backend/ResetPasswordController.php',
-];
$ignoreErrors[] = [
'message' => '#^Method Bolt\\\\Controller\\\\Backend\\\\ResetPasswordController\\:\\:buildResetEmail\\(\\) has parameter \\$resetToken with no type specified\\.$#',
'identifier' => 'missingType.parameter',
diff --git a/phpunit.xml.dist b/phpunit.xml.dist
index 37b5d0cdd..45dd4aa6c 100644
--- a/phpunit.xml.dist
+++ b/phpunit.xml.dist
@@ -22,7 +22,7 @@
-
+
diff --git a/src/Canonical.php b/src/Canonical.php
index 68f670a7a..a2fac3d3c 100644
--- a/src/Canonical.php
+++ b/src/Canonical.php
@@ -58,6 +58,16 @@ public function setRequest(?Request $request = null): void
$requestUrl = parse_url($this->request->getSchemeAndHttpHost());
+ // Handle test environment or malformed URLs
+ if ($requestUrl === false || ! isset($requestUrl['scheme'])) {
+ $this->setScheme('http');
+ $this->setHost('localhost');
+ $this->setPort(null);
+ $_SERVER['CANONICAL_HOST'] = 'localhost';
+ $_SERVER['CANONICAL_SCHEME'] = 'http';
+ return;
+ }
+
$configCanonical = (string) $this->config->get('general/canonical', $this->getRequest()->getSchemeAndHttpHost());
if (mb_strpos($configCanonical, 'http') !== 0) {
@@ -66,6 +76,16 @@ public function setRequest(?Request $request = null): void
$configUrl = parse_url($configCanonical);
+ // Handle malformed canonical URL
+ if ($configUrl === false || ! isset($configUrl['scheme']) || ! isset($configUrl['host'])) {
+ $this->setScheme('http');
+ $this->setHost('localhost');
+ $this->setPort(null);
+ $_SERVER['CANONICAL_HOST'] = 'localhost';
+ $_SERVER['CANONICAL_SCHEME'] = 'http';
+ return;
+ }
+
$this->setScheme($configUrl['scheme']);
$this->setHost($configUrl['host']);
$this->setPort($configUrl['port'] ?? null);
diff --git a/src/Controller/Backend/ResetPasswordController.php b/src/Controller/Backend/ResetPasswordController.php
index e8a305f57..ecdefd7ae 100644
--- a/src/Controller/Backend/ResetPasswordController.php
+++ b/src/Controller/Backend/ResetPasswordController.php
@@ -4,6 +4,7 @@
namespace Bolt\Controller\Backend;
+use Bolt\Collection\DeepCollection;
use Bolt\Configuration\Config;
use Bolt\Controller\TwigAwareController;
use Bolt\Entity\User;
@@ -180,7 +181,7 @@ protected function processSendingPasswordResetEmail(string $emailFormData, Maile
return $this->redirectToRoute('bolt_check_email');
}
- protected function buildResetEmail(array $config, $user, $resetToken): Email
+ protected function buildResetEmail(DeepCollection $config, $user, $resetToken): Email
{
return (new TemplatedEmail())
->from(new Address($config['mail_from'], $config['mail_name']))
diff --git a/tests/php/Controller/Backend/ResetPasswordControllerTest.php b/tests/php/Controller/Backend/ResetPasswordControllerTest.php
new file mode 100644
index 000000000..1706f8503
--- /dev/null
+++ b/tests/php/Controller/Backend/ResetPasswordControllerTest.php
@@ -0,0 +1,39 @@
+get()
+ * returned DeepCollection instead of array
+ *
+ * The bug was in Canonical::setRequest() which didn't handle test environment properly,
+ * causing parse_url() to return false/incomplete array, leading to TypeError when
+ * trying to access array keys.
+ */
+class ResetPasswordControllerTest extends DbAwareTestCase
+{
+ /**
+ * Test that the reset password page loads successfully.
+ * This is the main regression test - the bug prevented this page from loading.
+ */
+ public function testResetPasswordPageLoads(): void
+ {
+ // Navigate directly to reset password page (this is what the "Forgotten password" link does)
+ $crawler = $this->client->request('GET', '/bolt/reset-password');
+
+ // Verify page loads successfully (this would fail with TypeError before the fix)
+ $this->assertResponseIsSuccessful();
+ $this->assertRouteSame('bolt_forgot_password_request');
+
+ // Verify the form exists with email input
+ $this->assertSelectorExists('form');
+ $this->assertSelectorExists('input[name="reset_password_request_form[email]"]');
+ }
+}