Repository: Codeception/module-yii2
Pull request #128: Improve exception handling and code consistency
Author: login: TavoNiievez
Labels: name: released
Created: 2025-02-26T17:13:41Z | Merged: 2025-03-02T16:00:38Z



Diff:
diff --git a/src/Codeception/Lib/Connector/Yii2.php b/src/Codeception/Lib/Connector/Yii2.php
index 8be068c..8662836 100644
--- a/src/Codeception/Lib/Connector/Yii2.php
+++ b/src/Codeception/Lib/Connector/Yii2.php
@@ -10,6 +10,7 @@
 use Codeception\Lib\Connector\Yii2\TestMailer;
 use Codeception\Util\Debug;
 use InvalidArgumentException;
+use RuntimeException;
 use Symfony\Component\BrowserKit\AbstractBrowser as Client;
 use Symfony\Component\BrowserKit\Cookie;
 use Symfony\Component\BrowserKit\CookieJar;
@@ -142,14 +143,14 @@ protected function getApplication(): \yii\base\Application
         if (! isset(Yii::$app)) {
             $this->startApp();
         }
-        return Yii::$app ?? throw new \RuntimeException('Failed to create Yii2 application');
+        return Yii::$app ?? throw new RuntimeException('Failed to create Yii2 application');
     }
 
     private function getWebRequest(): YiiRequest
     {
         $request = $this->getApplication()->request;
         if (! $request instanceof YiiRequest) {
-            throw new \RuntimeException('Request component is not of type ' . YiiRequest::class);
+            throw new RuntimeException('Request component is not of type ' . YiiRequest::class);
         }
         return $request;
     }
@@ -172,8 +173,7 @@ public function resetApplication(bool $closeSession = true): void
      * Finds and logs in a user
      *
      * @internal
-     * @throws   ConfigurationException
-     * @throws   \RuntimeException
+     * @throws   ConfigurationException|RuntimeException
      */
     public function findAndLoginUser(int|string|IdentityInterface $user): void
     {
@@ -183,15 +183,9 @@ public function findAndLoginUser(int|string|IdentityInterface $user): void
             throw new ConfigurationException('The user component is not configured');
         }
 
-        if ($user instanceof IdentityInterface) {
-            $identity = $user;
-        } else {
-            // class name implementing IdentityInterface
-            $identityClass = $userComponent->identityClass;
-            $identity = $identityClass::findIdentity($user);
-            if ($identity === null) {
-                throw new \RuntimeException('User not found');
-            }
+        $identity = $user instanceof IdentityInterface ? $user : ($userComponent->identityClass)::findIdentity($user);
+        if ($identity === null) {
+            throw new RuntimeException('User not found');
         }
         $userComponent->login($identity);
     }
@@ -273,7 +267,7 @@ function ($matches) use (&$parameters): string {
             );
         }
         if ($template === null) {
-            throw new \RuntimeException("Failed to parse domain regex");
+            throw new RuntimeException("Failed to parse domain regex");
         }
         $template = preg_quote($template);
         $template = strtr($template, $parameters);
@@ -411,9 +405,9 @@ public function doRequest(object $request): Response
 
         $content = ob_get_clean();
         if (empty($content) && ! empty($yiiResponse->content) && ! isset($yiiResponse->stream)) {
-            throw new \RuntimeException('No content was sent from Yii application');
+            throw new RuntimeException('No content was sent from Yii application');
         } elseif ($content === false) {
-            throw new \RuntimeException('Failed to get output buffer');
+            throw new RuntimeException('Failed to get output buffer');
         }
 
         return new Response($content, $yiiResponse->statusCode, $yiiResponse->getHeaders()->toArray());
diff --git a/src/Codeception/Lib/Connector/Yii2/Logger.php b/src/Codeception/Lib/Connector/Yii2/Logger.php
index 69ebd4e..2443c39 100644
--- a/src/Codeception/Lib/Connector/Yii2/Logger.php
+++ b/src/Codeception/Lib/Connector/Yii2/Logger.php
@@ -5,6 +5,7 @@
 namespace Codeception\Lib\Connector\Yii2;
 
 use Codeception\Util\Debug;
+use SplQueue;
 use yii\base\Exception as YiiException;
 use yii\helpers\VarDumper;
 use yii\log\Logger as YiiLogger;
@@ -12,9 +13,9 @@
 final class Logger extends YiiLogger
 {
     /**
-     * @var \SplQueue<string>
+     * @var SplQueue<string>
      */
-    private \SplQueue $logQueue;
+    private SplQueue $logQueue;
 
     /**
      * @param array<string, mixed> $config
@@ -24,7 +25,7 @@ public function __construct(
         array $config = []
     ) {
         parent::__construct($config);
-        $this->logQueue = new \SplQueue();
+        $this->logQueue = new SplQueue();
     }
 
     public function init(): void
@@ -69,7 +70,7 @@ public function log($message, $level, $category = 'application'): void
     public function getAndClearLog(): string
     {
         $logs = iterator_to_array($this->logQueue);
-        $this->logQueue = new \SplQueue();
+        $this->logQueue = new SplQueue();
         return implode(PHP_EOL, $logs) . PHP_EOL;
     }
 }
diff --git a/src/Codeception/Lib/Connector/Yii2/TransactionForcer.php b/src/Codeception/Lib/Connector/Yii2/TransactionForcer.php
index e93ac44..1322f86 100644
--- a/src/Codeception/Lib/Connector/Yii2/TransactionForcer.php
+++ b/src/Codeception/Lib/Connector/Yii2/TransactionForcer.php
@@ -29,7 +29,7 @@ final class TransactionForcer extends ConnectionWatcher
     private array $transactions = [];
 
     public function __construct(
-        private bool $ignoreCollidingDSN
+        private readonly bool $ignoreCollidingDSN
     ) {
         parent::__construct();
     }
@@ -45,12 +45,12 @@ protected function connectionOpened(Connection $connection): void
         $key = md5(
             json_encode(
                 [
-                'dsn' => $connection->dsn,
-                'user' => $connection->username,
-                'pass' => $connection->password,
-                'attributes' => $connection->attributes,
-                'emulatePrepare' => $connection->emulatePrepare,
-                'charset' => $connection->charset,
+                    'dsn' => $connection->dsn,
+                    'user' => $connection->username,
+                    'pass' => $connection->password,
+                    'attributes' => $connection->attributes,
+                    'emulatePrepare' => $connection->emulatePrepare,
+                    'charset' => $connection->charset,
                 ],
                 JSON_THROW_ON_ERROR
             )
@@ -87,9 +87,6 @@ protected function connectionOpened(Connection $connection): void
 
     public function rollbackAll(): void
     {
-        /**
-         * @var Transaction $transaction
-         */
         foreach ($this->transactions as $transaction) {
             if ($transaction->db->isActive) {
                 $transaction->rollBack();
diff --git a/src/Codeception/Module/Yii2.php b/src/Codeception/Module/Yii2.php
index 4abfb65..54c289d 100644
--- a/src/Codeception/Module/Yii2.php
+++ b/src/Codeception/Module/Yii2.php
@@ -15,6 +15,7 @@
 use Codeception\Lib\Interfaces\ActiveRecord;
 use Codeception\Lib\Interfaces\PartedModule;
 use Codeception\TestInterface;
+use Exception;
 use PHPUnit\Framework\Assert;
 use ReflectionClass;
 use RuntimeException;
@@ -276,7 +277,9 @@ public function _initialize(): void
 
         $this->defineConstants();
         $this->server = $_SERVER;
-        $this->initServerGlobal();
+        // Adds the required server params. Note this is done separately from the request cycle since someone might call
+        // `Url::to` before doing a request, which would instantiate the request component with incorrect server params.
+        $_SERVER = [...$_SERVER, $this->getServerParams()];
     }
 
     /**
@@ -294,26 +297,27 @@ protected function onReconfigure(): void
     }
 
     /**
-     * Adds the required server params.
-     * Note this is done separately from the request cycle since someone might call
-     * `Url::to` before doing a request, which would instantiate the request component with incorrect server params.
+     * @return array{
+     *     SCRIPT_FILENAME: string,
+     *     SCRIPT_NAME: string,
+     *     SERVER_NAME: string,
+     *     SERVER_PORT: string|int,
+     *     HTTPS: bool
+     * }
      */
-    private function initServerGlobal(): void
+    private function getServerParams(): array
     {
         $entryUrl = $this->config['entryUrl'];
         $parsedUrl = parse_url($entryUrl);
         $entryFile = $this->config['entryScript'] ?: basename($entryUrl);
         $entryScript = $this->config['entryScript'] ?: ($parsedUrl['path'] ?? '');
-        $_SERVER = array_merge(
-            $_SERVER,
-            [
+        return [
             'SCRIPT_FILENAME' => $entryFile,
             'SCRIPT_NAME' => $entryScript,
             'SERVER_NAME' => $parsedUrl['host'] ?? '',
             'SERVER_PORT' => $parsedUrl['port'] ?? '80',
             'HTTPS' => isset($parsedUrl['scheme']) && $parsedUrl['scheme'] === 'https',
-            ]
-        );
+        ];
     }
 
     /**
@@ -335,23 +339,24 @@ protected function validateConfig(): void
                 "The application config file does not exist: " . $pathToConfig,
             );
         }
-        $validMethods = implode(", ", Yii2Connector::CLEAN_METHODS);
+        $validCleanMethods = implode(", ", Yii2Connector::CLEAN_METHODS);
         if (! in_array($this->config['responseCleanMethod'], Yii2Connector::CLEAN_METHODS, true)) {
             throw new ModuleConfigException(
                 self::class,
-                "The response clean method must be one of: " . $validMethods,
+                "The response clean method must be one of: " . $validCleanMethods,
             );
         }
+        $validMailMethods = implode(", ", Yii2Connector::MAIL_METHODS);
         if (! in_array($this->config['mailMethod'], Yii2Connector::MAIL_METHODS, true)) {
             throw new ModuleConfigException(
                 self::class,
-                "The mail method must be one of: " . $validMethods
+                "The mail method must be one of: " . $validMailMethods
             );
         }
         if (! in_array($this->config['requestCleanMethod'], Yii2Connector::CLEAN_METHODS, true)) {
             throw new ModuleConfigException(
                 self::class,
-                "The request clean method must be one of: " . $validMethods,
+                "The request clean method must be one of: " . $validCleanMethods,
             );
         }
     }
@@ -377,19 +382,7 @@ private function configureClient(array $settings): void
      */
     protected function recreateClient(): void
     {
-        $entryUrl = $this->config['entryUrl'];
-        $parsedUrl = parse_url($entryUrl);
-        $entryFile = $this->config['entryScript'] ?: basename($entryUrl);
-        $entryScript = $this->config['entryScript'] ?: ($parsedUrl['path'] ?? '');
-        $this->client = new Yii2Connector(
-            [
-            'SCRIPT_FILENAME' => $entryFile,
-            'SCRIPT_NAME' => $entryScript,
-            'SERVER_NAME' => $parsedUrl['host'] ?? '',
-            'SERVER_PORT' => $parsedUrl['port'] ?? '80',
-            'HTTPS' => isset($parsedUrl['scheme']) && $parsedUrl['scheme'] === 'https',
-            ]
-        );
+        $this->client = new Yii2Connector($this->getServerParams());
         $this->validateConfig();
         $this->configureClient($this->config);
     }
@@ -463,7 +456,7 @@ public function _after(TestInterface $test): void
     }
 
     /**
-     * @param \Exception $fail
+     * @param Exception $fail
      */
     public function _failed(TestInterface $test, $fail): void
     {


Reviews:
user: login: SamMousa
body: Reviewed from mobile, might not catch all.
Lgtm. I'd love a code sniffer rule for the import of global classes Vs the `\xxx` notation.

state: APPROVED
author_association: COLLABORATOR
submitted_at: 2025-02-26T17:51:16Z
user: login: Arhell
body: @samdark check please
state: APPROVED
author_association: MEMBER
submitted_at: 2025-02-27T00:04:53Z
user: login: TavoNiievez
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-02-28T17:42:25Z
user: login: TavoNiievez
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-02-28T17:42:43Z
user: login: samdark
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-01T10:03:40Z

Review comments:
diff_hunk: @@ -276,7 +277,9 @@ public function _initialize(): void
 
         $this->defineConstants();
         $this->server = $_SERVER;
-        $this->initServerGlobal();
+        // Adds the required server params. Note this is done separately from the request cycle since someone might call
+        // `Url::to` before doing a request, which would instantiate the request component with incorrect server params.
+        $_SERVER = array_merge($_SERVER, $this->getServerParams());
path: src/Codeception/Module/Yii2.php
user: login: SamMousa
body: Use array destructuring
```suggestion
        $_SERVER = [...$_SERVER, $this->getServerParams()];
```
created_at: 2025-02-26T17:47:20Z
author_association: COLLABORATOR
diff_hunk: @@ -294,26 +297,21 @@ protected function onReconfigure(): void
     }
 
     /**
-     * Adds the required server params.
-     * Note this is done separately from the request cycle since someone might call
-     * `Url::to` before doing a request, which would instantiate the request component with incorrect server params.
+     * @return array<string, mixed>
path: src/Codeception/Module/Yii2.php
user: login: SamMousa
body: Isn't every value a string? We can be even more specific and use array shape.
created_at: 2025-02-26T17:48:21Z
author_association: COLLABORATOR
diff_hunk: @@ -294,26 +297,21 @@ protected function onReconfigure(): void
     }
 
     /**
-     * Adds the required server params.
-     * Note this is done separately from the request cycle since someone might call
-     * `Url::to` before doing a request, which would instantiate the request component with incorrect server params.
+     * @return array<string, mixed>
path: src/Codeception/Module/Yii2.php
user: login: TavoNiievez
body: It should be, but it could be misconfigured.

Since it is possible to write any type of value in $this->config['entryScript'] and we are not validating the type of data received anywhere I get what you see in the screenshot.

![imagen](https://github.com/user-attachments/assets/d48bb976-c74b-4621-8952-00bf754abe4e)

I changed it to use array shape, I hope that's what you meant.

the port was returned manually as string, but from `parse_url` it is returned as int, let me know if you prefer an explicit cast to string `(string)` or if you are satisfied with `string|int`.
created_at: 2025-02-28T17:42:25Z
author_association: MEMBER
diff_hunk: @@ -276,7 +277,9 @@ public function _initialize(): void
 
         $this->defineConstants();
         $this->server = $_SERVER;
-        $this->initServerGlobal();
+        // Adds the required server params. Note this is done separately from the request cycle since someone might call
+        // `Url::to` before doing a request, which would instantiate the request component with incorrect server params.
+        $_SERVER = array_merge($_SERVER, $this->getServerParams());
path: src/Codeception/Module/Yii2.php
user: login: TavoNiievez
body: Done ✅.
created_at: 2025-02-28T17:42:43Z
author_association: MEMBER
diff_hunk: @@ -257,27 +251,16 @@ public function clearEmails(): void
      */
     private function getDomainRegex(string $template): string
     {
-        if (preg_match('#https?://(.*)#', $template, $matches)) {
-            $template = $matches[1];
-        }
-        $parameters = [];
+        $template = preg_match('#https?://(.*)#', $template, $m) ? $m[1] : $template;
path: src/Codeception/Lib/Connector/Yii2.php
user: login: samdark
body: It was cleaner without ternary operator, I think.
created_at: 2025-03-01T10:01:26Z
author_association: MEMBER
diff_hunk: @@ -257,27 +251,16 @@ public function clearEmails(): void
      */
     private function getDomainRegex(string $template): string
     {
-        if (preg_match('#https?://(.*)#', $template, $matches)) {
-            $template = $matches[1];
-        }
-        $parameters = [];
+        $template = preg_match('#https?://(.*)#', $template, $m) ? $m[1] : $template;
+        $params = [];
         if (str_contains($template, '<')) {
-            $template = preg_replace_callback(
-                '/<(?:\w+):?([^>]+)?>/u',
-                function ($matches) use (&$parameters): string {
-                    $key = '__' . count($parameters) . '__';
-                    $parameters[$key] = $matches[1] ?? '\w+';
-                    return $key;
-                },
-                $template,
-            );
-        }
-        if ($template === null) {
-            throw new \RuntimeException("Failed to parse domain regex");
+            $template = preg_replace_callback('/<(?:\w+):?([^>]+)?>/u', function ($m) use (&$params) {
+                $key = '__' . count($params) . '__';
+                $params[$key] = $m[1] ?? '\w+';
+                return $key;
+            }, $template) ?? throw new RuntimeException("Failed to parse domain regex");
path: src/Codeception/Lib/Connector/Yii2.php
user: login: samdark
body: That became quite hard to read. Let's not overuse ternaries please.
created_at: 2025-03-01T10:02:18Z
author_association: MEMBER

PR comments:
user: login: SamMousa
created_at: 2025-02-28T17:45:00Z
body: Will review soon, reminder: use conventional commit messages. https://www.conventionalcommits.org/en/v1.0.0/

Otherwise automated releases won't work. For now I can fix it in the merge commit message.
author_association: COLLABORATOR
user: login: samdark
created_at: 2025-03-01T10:04:38Z
body:  Overall cosmetic changes with a few changes for behavior that I see as good ones :+1: 
author_association: MEMBER
user: login: SamMousa
created_at: 2025-03-02T16:00:34Z
body: LGTM
author_association: COLLABORATOR
user: login: github-actions[bot]
created_at: 2025-03-02T16:01:46Z
body: :tada: This PR is included in version 2.0.2 :tada:

The release is available on [GitHub release](https://github.com/Codeception/module-yii2/releases/tag/v2.0.2)

Your **[semantic-release](https://github.com/semantic-release/semantic-release)** bot :package::rocket:
author_association: NONE