Repository: Codeception/Codeception (php)

Issue:
Array-parameters are passed incorrectly
#### I have environment params:
    // file env.yml
    products:
      booking:
        - 123456
        - 234567
        - 345678

#### Then I use them in the acceptance.suite.yml
    modules:
      enabled:
        - ...
        - \Helper\Acceptance
      config:
          // ......
        \Helper\Acceptance:
          products:
             booking:
               %products.booking%

#### Then in the test I try to get those params:
    class SomeCest
    {
        /** @var Acceptance */
        protected $helper;

        public function _inject(Acceptance $helper)
        {
            $this->helper = $helper;
        }

        // tests
        public function tryToTest(AcceptanceTester $I)
        {
            $products = $this->helper->_getConfig('products');
            codecept_debug($products['booking']);
        }
    }

#### But in the console I see the following:
    PHP Notice:  Array to string conversion in .../vendor/codeception/codeception/src/Codeception/Util/Template.php on line 83
    Array
    (
      [0] => Array
    )

#### The reason is:
The method `\Codeception\Util\Template::produce` tries to replace a parameter's placeholder with an array:
```    
$result = str_replace($this->placeholderStart . $placeholder . $this->placeholderEnd, $value, $result);
```
where `$value` is my array from the environment.

#### So I'm forced to use this configuration (in the acceptance.suite.yml):
      products:
        booking:
          - %products.booking.0%
          - %products.booking.1%
          - %products.booking.2%

But this is inconvenient and not the Feng Shui.

Could you fix this issue?
Or is there some other way to pass array in the test?
Thank you.

P.S. Non-array parameters are passed normal.
[5.0-RC1] Injected Int typed parameter stop test execution
Codeception Version: 5.0-RC1
PHP: 8.1

I tested  the newest version of Codeception 5 and hit a PHP 8.1 deprecation resulting in a test crash.
We inject some parameters into the `codeception.yml` config

```yml
modules:
  config:
    - Redis:
        host: '%redis.host%'
        port: '%redis.port%'
```

The port is an int value and this creates a deprecation here:

https://github.com/Codeception/Codeception/blob/5.0/src/Codeception/Util/Template.php#L80

A cast to string would be an easy change but not sure if this is always correct. Likely the output of Codeception 4 was already converted to a string but I did not check it.

Pull request description:
Encode non-string parameters in configuration files
This change allows to use non-string values in parameterized configuration files.
Most importantly objects and arrays, but also integers, floats and booleans.

Because unquoted placeholder `%var%` is not a valid value in Yaml format, this change will replace quoted placeholders with unquoted values,   e.g .  `param: "%var%"` will be rendered as `param: false`

Fixes #6408 
Fixes #5223

Patch:
diff --git a/src/Codeception/Configuration.php b/src/Codeception/Configuration.php
index 76b6cbb9c..ac638fb29 100644
--- a/src/Codeception/Configuration.php
+++ b/src/Codeception/Configuration.php
@@ -408,9 +408,18 @@ protected static function loadEnvConfigs(string $path): array
     protected static function getConfFromContents(string $contents, string $filename = '(.yml)'): array
     {
         if (self::$params) {
-            $template = new Template($contents, '%', '%');
-            $template->setVars(self::$params);
-            $contents = $template->produce();
+            // replace '%var%' with encoded value
+            $singleQuoteTemplate = new Template($contents, "'%", "%'", 'json_encode');
+            $singleQuoteTemplate->setVars(self::$params);
+            $contents = $singleQuoteTemplate->produce();
+            // replace "%var%" with encoded value
+            $doubleQuoteTemplate = new Template($contents, '"%', '%"', 'json_encode');
+            $doubleQuoteTemplate->setVars(self::$params);
+            $contents = $doubleQuoteTemplate->produce();
+            // replace %var% with string value as is
+            $plainTemplate = new Template($contents, '%', '%');
+            $plainTemplate->setVars(self::$params);
+            $contents = $plainTemplate->produce();
         }
 
         try {
diff --git a/src/Codeception/Util/Template.php b/src/Codeception/Util/Template.php
index 8f0c9e0d8..ab450a39e 100644
--- a/src/Codeception/Util/Template.php
+++ b/src/Codeception/Util/Template.php
@@ -16,16 +16,14 @@
  */
 class Template
 {
-    protected string $template;
-    protected array $vars = [];
-    protected string $placeholderStart;
-    protected string $placeholderEnd;
+    private array $vars = [];
 
-    public function __construct(string $template, string $placeholderStart = '{{', string $placeholderEnd = '}}')
-    {
-        $this->template         = $template;
-        $this->placeholderStart = $placeholderStart;
-        $this->placeholderEnd   = $placeholderEnd;
+    public function __construct(
+        private string $template,
+        private string $placeholderStart = '{{',
+        private string $placeholderEnd = '}}',
+        private ?string $encoderFunction = null,
+    ) {
     }
 
     /**
@@ -69,7 +67,8 @@ public function produce(): string
         foreach ($matches as $match) { // fill in placeholders
             $placeholder = $match[1];
             $value = $this->vars;
-            foreach (explode('.', $placeholder) as $segment) {
+
+            foreach (explode('.', trim($placeholder, '\'"')) as $segment) {
                 if (is_array($value) && array_key_exists($segment, $value)) {
                     $value = $value[$segment];
                 } else {
@@ -77,6 +76,12 @@ public function produce(): string
                 }
             }
 
+            if ($this->encoderFunction !== null) {
+                $value = ($this->encoderFunction)($value);
+            } elseif (!is_string($value)) {
+                $value = (string)$value;
+            }
+
             $result = str_replace($this->placeholderStart . $placeholder . $this->placeholderEnd, $value, $result);
         }
         return $result;


Test patch:
diff --git a/tests/cli/ConfigParamsCest.php b/tests/cli/ConfigParamsCest.php
index 025108348..02df0acbd 100644
--- a/tests/cli/ConfigParamsCest.php
+++ b/tests/cli/ConfigParamsCest.php
@@ -7,28 +7,28 @@ final class ConfigParamsCest
     public function checkYamlParamsPassed(CliGuy $I)
     {
         $I->amInPath('tests/data/params');
-        $I->executeCommand('run -c codeception_yaml.yml');
+        $I->executeCommand('run -c codeception_yaml.yml dummy');
         $I->seeInShellOutput('OK (1 test');
     }
 
     public function checkDotEnvParamsPassed(CliGuy $I)
     {
         $I->amInPath('tests/data/params');
-        $I->executeCommand('run -c codeception_dotenv.yml');
+        $I->executeCommand('run -c codeception_dotenv.yml dummy');
         $I->seeInShellOutput('OK (1 test');
     }
 
     public function checkComplexDotEnvParamsPassed(CliGuy $I)
     {
         $I->amInPath('tests/data/params');
-        $I->executeCommand('run -c codeception_dotenv2.yml');
+        $I->executeCommand('run -c codeception_dotenv2.yml dummy');
         $I->seeInShellOutput('OK (1 test');
     }
 
     public function checkEnvParamsPassed(CliGuy $I)
     {
         $I->amInPath('tests/data/params');
-        $I->executeCommand('run --no-exit');
+        $I->executeCommand('run --no-exit dummy');
         $I->seeInShellOutput('FAILURES');
         $I->seeInShellOutput("Failed asserting that an array contains 'val1'");
     }
@@ -36,14 +36,21 @@ public function checkEnvParamsPassed(CliGuy $I)
     public function checkParamsPassedInSelf(CliGuy $I)
     {
         $I->amInPath('tests/data/params');
-        $I->executeCommand('run -c codeception_self.yml');
+        $I->executeCommand('run -c codeception_self.yml dummy');
         $I->seeInShellOutput('OK (1 test');
     }
 
     public function checkXmlParamsPassed(CliGuy $I)
     {
         $I->amInPath('tests/data/params');
-        $I->executeCommand('run -c codeception_xml.yml');
+        $I->executeCommand('run -c codeception_xml.yml dummy');
+        $I->seeInShellOutput('OK (1 test');
+    }
+
+    public function checkNonStringParamsAreEncodedProperly(CliGuy $I)
+    {
+        $I->amInPath('tests/data/params');
+        $I->executeCommand('run -c codeception_yaml.yml complex');
         $I->seeInShellOutput('OK (1 test');
     }
 }
diff --git a/tests/data/params/params.yml b/tests/data/params/params.yml
index 07a65d2db..391a3458d 100644
--- a/tests/data/params/params.yml
+++ b/tests/data/params/params.yml
@@ -1,3 +1,8 @@
 parameters:
     KEY1: val1
-    KEY2: val2
\ No newline at end of file
+    KEY2: val2
+    KEY3: 1
+    KEY4:
+        - foo
+        - bar
+        - baz
\ No newline at end of file
diff --git a/tests/data/params/tests/_support/ComplexTester.php b/tests/data/params/tests/_support/ComplexTester.php
new file mode 100644
index 000000000..93265ee48
--- /dev/null
+++ b/tests/data/params/tests/_support/ComplexTester.php
@@ -0,0 +1,27 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * Inherited Methods
+ * @method void wantToTest($text)
+ * @method void wantTo($text)
+ * @method void execute($callable)
+ * @method void expectTo($prediction)
+ * @method void expect($prediction)
+ * @method void amGoingTo($argumentation)
+ * @method void am($role)
+ * @method void lookForwardTo($achieveValue)
+ * @method void comment($description)
+ * @method void pause($vars = [])
+ *
+ * @SuppressWarnings(PHPMD)
+*/
+class ComplexTester extends \Codeception\Actor
+{
+    use _generated\ComplexTesterActions;
+
+    /**
+     * Define custom actions here
+     */
+}
diff --git a/tests/data/params/tests/_support/Helper/Dummy.php b/tests/data/params/tests/_support/Helper/Dummy.php
index 12ac312b1..cf77eea59 100644
--- a/tests/data/params/tests/_support/Helper/Dummy.php
+++ b/tests/data/params/tests/_support/Helper/Dummy.php
@@ -22,4 +22,9 @@ public function seeVarsAreSet()
         $this->assertContains('val1', $vars);
         $this->assertContains('val2', $vars);
     }
+
+    public function seeVarEquals(int $position, $value)
+    {
+        $this->assertSame($value, $this->config['vars'][$position]);
+    }
 }
diff --git a/tests/data/params/tests/complex.suite.yml b/tests/data/params/tests/complex.suite.yml
new file mode 100644
index 000000000..a6fb5f0c8
--- /dev/null
+++ b/tests/data/params/tests/complex.suite.yml
@@ -0,0 +1,5 @@
+actor: ComplexTester
+modules:
+    enabled:
+        - \Helper\Dummy:
+              vars: ["%KEY3%", '%KEY4%', "%KEY4%", "%KEY4.1%"]
\ No newline at end of file
diff --git a/tests/data/params/tests/complex/YamlCept.php b/tests/data/params/tests/complex/YamlCept.php
new file mode 100644
index 000000000..ece5d7fc0
--- /dev/null
+++ b/tests/data/params/tests/complex/YamlCept.php
@@ -0,0 +1,8 @@
+<?php
+
+$I = new DummyTester($scenario);
+
+$I->seeVarEquals(0, 1);
+$I->seeVarEquals(1, ['foo', 'bar', 'baz']);
+$I->seeVarEquals(2, ['foo', 'bar', 'baz']);
+$I->seeVarEquals(3, 'bar');
diff --git a/tests/data/params/tests/dummy.suite.yml b/tests/data/params/tests/dummy.suite.yml
index 54f682dab..28761f76e 100644
--- a/tests/data/params/tests/dummy.suite.yml
+++ b/tests/data/params/tests/dummy.suite.yml
@@ -2,5 +2,5 @@ actor: DummyTester
 modules:
     enabled:
         - \Helper\Dummy:
-            vars: ["%KEY1%", "%KEY2%"]
-            path: "%PATH%"
\ No newline at end of file
+            vars: ["%KEY1%", '%KEY2%', '%KEY3%', '%KEY4%']
+            path: "%PATH%"