GherkinParam::onReconfigure()   A
last analyzed

Complexity

Conditions 1
Paths 1

Size

Total Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 4
rs 10
c 0
b 0
f 0
cc 1
nc 1
nop 0
1
<?php
2
3
declare(strict_types=1);
4
5
/**
6
 * Before step hook that provide parameter syntax notation
7
 * for accessing fixture data between Gherkin steps/tests
8
 * example:
9
 *  I see "{{param}}"
10
 *  {{param}} will be replaced by the value of Fixtures::get('param')
11
 *
12
 */
13
namespace Codeception\Extension;
14
15
use \Codeception\Util\Fixtures;
16
use \Behat\Gherkin\Node\TableNode;
17
use \ReflectionProperty;
18
use \RuntimeException;
19
use \Codeception\Exception\ExtensionException;
20
use \Codeception\Configuration;
21
use \Codeception\Step;
22
use \Codeception\Extension\GherkinParamException;
23
24
class GherkinParam extends \Codeception\Module
25
{
26
 
27
  /**
28
   * @var boolean Flag to enable exception (prioritised over $nullable=true)
29
   * false: no exception thrown if parameter invalid, instead replacement value is parameter {{name}} 
30
   * true: exception thrown if parameter invalid
31
   */
32
  private $throwException = false;
33
34
  /**
35
   * @var boolean Flag to null invalid parameter (incompatible with $throwException=true)
36
   * true: if parameter invalid then replacement value will be null
37
   * false: default behaviour, ie replacement value is parameter {{name}} 
38
   */
39
  private $nullable = false;
40
41
  protected $config = ['onErrorThrowException', 'onErrorNull'];
42
43
  protected $requiredFields = [];
44
45
  /**
46
   * @var array List events to listen to
47
   */
48
  public static $events = [
49
    //run before any suite
50
    'suite.before' => 'beforeSuite',
51
    //run before any steps
52
    'step.before' => 'beforeStep'
53
  ];
54
55
  /**
56
   * @var array Current test suite config
57
   */
58
  private static $suiteConfig;
59
60
  /**
61
   * @var array RegExp for parsing steps
62
   */
63
  private static $regEx = [
64
    'match'  => '/{{\s?[A-z0-9_:-<>]+\s?}}/',
65
    'filter' => '/[{}]/',
66
    'config' => '/(?:^config)?:([A-z0-9_-]+)+(?=:|$)/',
67
    'array'  => '/^(?P<var>[A-z0-9_-]+)(?:\[(?P<key>.+)])$/'
68
  ];
69
70
  /**
71
   * Initialize module configuration
72
   */
73
  final public function _initialize() 
74
  {
75
    if (isset($this->config['onErrorThrowException'])) {
76
      $this->throwException = (bool) $this->config['onErrorThrowException'];
77
    }
78
79
    if (isset($this->config['onErrorNull'])) {
80
      $this->nullable = (bool) $this->config['onErrorNull'];
81
    }
82
  }
83
84
  /**
85
   * Dynamic module reconfiguration
86
   */
87
  final public function onReconfigure()
88
  {
89
    $this->_initialize();
90
  }
91
92
  /**
93
   * Parse param and replace {{.*}} by its Fixtures::get() value if exists
94
   *
95
   * @param string $param
96
   *
97
   * @return \mixed|null Returns parameter's value if exists, else parameter's name
98
   */
99
  final protected function getValueFromParam(string $param)
100
  {
101
    if (preg_match_all(self::$regEx['match'], $param, $matches)) {
102
      try {
103
        $values = [];
104
        $matches = $matches[0]; // override for readability
105
        foreach ($matches as $variable) {
106
          $variable = trim(preg_filter(self::$regEx['filter'], '', $variable));
107
          // config case
108
          if (preg_match(self::$regEx['config'], $variable)) {
109
            $values[] = $this->getValueFromConfig($variable);
110
          } 
111
          // array case
112
          elseif (preg_match(self::$regEx['array'], $variable)) {
113
            try {
114
              $values[] = $this->getValueFromArray($variable);
115
            } catch (RuntimeException $e) {
116
              if ($this->throwException) throw new GherkinParamException();
117
              if ($this->nullable) $values[] = null;
118
            }
119
          } 
120
          // normal case
121
          else {
122
            try {
123
              $values[] = Fixtures::get($variable);
124
            } catch (RuntimeException $e) {
125
              if ($this->throwException) throw new GherkinParamException();
126
              if ($this->nullable) $values[] = null;
127
            }
128
          }
129
          // if machting value return is not found (null)
130
          if (is_null(end($values))) {
131
            if ($this->throwException) throw new GherkinParamException();
132
          }
133
        }
134
135
        // array str_replace cannot be used 
136
        // due to the default behavior when `search` and `replace` arrays size mismatch
137
        $param = $this->mapParametersToValues($matches, $values, $param);
138
139
      } catch (GherkinParamException $e) {
140
        // only active if throwException setting is true
141
        throw new ExtensionException(
142
          $this, 
143
          "Incorrect parameter `${param}` variable `${variable}` not found, or not initialized"
144
        );
145
      }
146
    
147
    }
148
149
    return $param;
150
  }
151
152
  /**
153
   * Replace parameters' matches by corresponding values
154
   *
155
   * @param array $matches
156
   * @param array $values
157
   * @param string $param
158
   *
159
   * @return \mixed|null Returns parameter's value if exists, else parameter {{name}}
160
   */  
161
  //TODO: pass param ref to function (&) [performance]
162
  final private function mapParametersToValues(array $matches, array $values, string $param)
163
  {
164
    $len = count($matches);
165
    for ($i = 0; $i < $len; $i++) {
166
      $search = $matches[$i];
167
      if (isset($values[$i])) {
168
        $replacement = $values[$i];
169
        if (is_array($replacement)) { 
170
          // case of replacement is an array (case of config param), ie param does not exists
171
          if ($this->throwException) {
172
            throw new GherkinParamException();
173
          }
174
          if ($this->nullable) {
175
            $param = null;
176
          }
177
          break;
178
        }
179
        //TODO: replace str_replace by strtr (performance)
180
        $param = str_replace($search, $replacement, $param);
181
      } else {
182
        if ($this->throwException) {
183
          throw new GherkinParamException();
184
        }
185
        if ($this->nullable) {
186
          $param = null;
187
        }
188
      }
189
    }
190
    return $param;
191
  }
192
193
  /**
194
   * Retrieve param value from current suite config
195
   *
196
   * @param string $param
197
   *
198
   * @return \mixed|null Returns parameter's value if exists, else null
199
   */
200
  //TODO: pass param ref to function (&) [performance]
201
  final protected function getValueFromConfig(string $param)
202
  {
203
    $value = null;
204
    $config = self::$suiteConfig;
205
206
    preg_match_all(self::$regEx['config'], $param, $args, PREG_PATTERN_ORDER);
207
    foreach ($args[1] as $arg) {
208
      if (array_key_exists($arg, $config)) {
209
        $value = $config[$arg];
210
        if (is_array($value)) {
211
          $config = $value;
212
        } else {
213
          break;
214
        }
215
      }
216
    }
217
    return $value;
218
  }
219
220
  /**
221
   * Retrieve param value from array in Fixtures
222
   *
223
   * @param string $param
224
   *
225
   * @return \mixed|null Returns parameter's value if exists, else null
226
   */
227
  //TODO: pass param ref to function (&) [performance]
228
  final protected function getValueFromArray(string $param)
229
  {
230
    $value = null;
231
232
    preg_match_all(self::$regEx['array'], $param, $args);
233
    $array = Fixtures::get($args['var'][0]);
234
    if (array_key_exists($args['key'][0], $array)) {
235
      $value = $array[$args['key'][0]];
236
    }
237
    return $value;
238
  }
239
240
  /**
241
   * Capture suite's config before any execution
242
   *
243
   * @param array $settings
244
   * @return void
245
   *
246
   * @codeCoverageIgnore
247
   * @ignore Codeception specific
248
   */
249
  final public function _beforeSuite($settings = [])
250
  {
251
    self::$suiteConfig = $settings;
252
  }
253
254
  /**
255
   * Parse scenario's step before execution
256
   *
257
   * @param \Codeception\Step $step
258
   * @return void
259
   */
260
  final public function _beforeStep(Step $step)
261
  {
262
    // access to the protected property using reflection
263
    $refArgs = new ReflectionProperty(get_class($step), 'arguments');
264
    // change property accessibility to public
265
    $refArgs->setAccessible(true);
266
    // retrieve 'arguments' value
267
    $args = $refArgs->getValue($step);
268
    foreach ($args as $index => $arg) {
269
      if (is_string($arg)) {
270
      // case if arg is a string
271
      // e.g. I see "{{param}}"
272
        $args[$index] = $this->getValueFromParam($arg);
273
      } elseif (is_a($arg, '\Behat\Gherkin\Node\TableNode')) {
274
      // case if arg is a table
275
      // e.g. I see :
276
      //  | paramater |
277
      //  | {{param}} |
278
        $prop = new ReflectionProperty(get_class($arg), 'table');
279
        $prop->setAccessible(true);
280
        $table = $prop->getValue($arg);
281
        foreach ($table as $i => $row) {
282
          foreach ($row as $j => $cell) {
283
            $val = $this->getValueFromParam($cell);
284
            $table[$i][$j] = $val ? $val : null; // issue TableNode does not support `null` values in table
285
          }
286
        }
287
        $prop->setValue($arg, $table);
288
        $prop->setAccessible(false);
289
        $args[$index] = $arg;
290
      } elseif (is_array($arg)) {
291
        foreach ($arg as $k => $v) {
292
          if (is_string($v)) {
293
            $args[$index][$k] = $this->getValueFromParam($v);
294
          }
295
        }
296
      }
297
    }
298
    // set new arguments value
299
    $refArgs->setValue($step, $args);
300
  }
301
302
}
303