Completed
Pull Request — master (#26)
by Greg
01:13
created

GherkinParam::onReconfigure()   A

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
23
class GherkinParam extends \Codeception\Module
24
{
25
 
26
  /**
27
   * @var boolean Flag to enable exception (prioritised over $nullable=true)
28
   * false: no exception thrown if parameter invalid, instead replacement value is parameter {{name}} 
29
   * true: exception thrown if parameter invalid
30
   */
31
  private $throwException = false;
32
33
  /**
34
   * @var boolean Flag to null invalid parameter (incompatible with $throwException=true)
35
   * true: if parameter invalid then replacement value will be null
36
   * false: default behaviour, ie replacement value is parameter {{name}} 
37
   */
38
  private $nullable = false;
39
40
  protected $config = ['onErrorThrowException', 'onErrorNull'];
41
42
  protected $requiredFields = [];
43
44
  /**
45
   * @var array List events to listen to
46
   */
47
  public static $events = [
48
    //run before any suite
49
    'suite.before' => 'beforeSuite',
50
    //run before any steps
51
    'step.before' => 'beforeStep'
52
  ];
53
54
  /**
55
   * @var array Current test suite config
56
   */
57
  private static $suiteConfig;
58
59
  /**
60
   * @var array RegExp for parsing steps
61
   */
62
  private static $regEx = [
63
    'match'  => '/{{\s?[A-z0-9_:-<>]+\s?}}/',
64
    'filter' => '/[{}]/',
65
    'config' => '/(?:^config)?:([A-z0-9_-]+)+(?=:|$)/',
66
    'array'  => '/^(?P<var>[A-z0-9_-]+)(?:\[(?P<key>.+)])$/'
67
  ];
68
69
  /**
70
   * Initialize module configuration
71
   */
72
  final public function _initialize() 
73
  {
74
    if (isset($this->config['onErrorThrowException'])) {
75
      $this->throwException = (bool) $this->config['onErrorThrowException'];
76
    }
77
78
    if (isset($this->config['onErrorNull'])) {
79
      $this->nullable = (bool) $this->config['onErrorNull'];
80
    }
81
  }
82
83
  /**
84
   * Dynamic module reconfiguration
85
   */
86
  final public function onReconfigure()
87
  {
88
    $this->_initialize();
89
  }
90
91
  /**
92
   * Parse param and replace {{.*}} by its Fixtures::get() value if exists
93
   *
94
   * @param string $param
95
   *
96
   * @return \mixed|null Returns parameter's value if exists, else parameter's name
97
   */
98
  final protected function getValueFromParam(string $param)
99
  {
100
    if (preg_match_all(self::$regEx['match'], $param, $matches)) {
101
      try {
102
        $values = [];
103
        $matches = $matches[0]; // override for readability
104
        foreach ($matches as $variable)
105
        {
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($this, "Incorrect parameter name ${param}, or not initialized");
142
      }
143
    
144
    }
145
146
    return $param;
147
  }
148
149
  /**
150
   * Replace parameters' matches by corresponding values
151
   *
152
   * @param array $matches
153
   * @param array $values
154
   * @param string $param
155
   *
156
   * @return \mixed|null Returns parameter's value if exists, else parameter {{name}}
157
   */  
158
  //TODO: pass param ref to function (&) [performance]
159
  final private function mapParametersToValues(array $matches, array $values, string $param)
160
  {
161
    //TODO: move count() into separate variable [performance]
162
    for ($i=0; $i<count($matches); $i++) {
0 ignored issues
show
Performance Best Practice introduced by
It seems like you are calling the size function count() as part of the test condition. You might want to compute the size beforehand, and not on each iteration.

If the size of the collection does not change during the iteration, it is generally a good practice to compute it beforehand, and not on each iteration:

for ($i=0; $i<count($array); $i++) { // calls count() on each iteration
}

// Better
for ($i=0, $c=count($array); $i<$c; $i++) { // calls count() just once
}
Loading history...
163
      $search = $matches[$i];
164
      if (\is_string($search)) { // if null then skip
165
        if (isset($values[$i])) {
166
          $replacement = $values[$i];
167
          if (\is_array($replacement)) { 
168
            // case of replacement is an array (case of config param), ie param does not exists
169
            if ($this->throwException) throw new GherkinParamException();
170
            if ($this->nullable) $param = null;
171
            break;
172
          }
173
          //TODO: replace str_replace by strtr (performance)
174
          $param = \str_replace($search, $replacement, $param);
175
        } else {
176
          if ($this->throwException) throw new GherkinParamException();
177
          if ($this->nullable) $param = null;
178
        }
179
      } else {
180
        if ($this->nullable) $param = null;
181
        break;
182
      }
183
    }
184
    return $param;
185
  }
186
187
  /**
188
   * Retrieve param value from current suite config
189
   *
190
   * @param string $param
191
   *
192
   * @return \mixed|null Returns parameter's value if exists, else null
193
   */
194
  //TODO: pass param ref to function (&) [performance]
195
  final protected function getValueFromConfig(string $param)
196
  {
197
    $value = null;
198
    $config = self::$suiteConfig;
199
200
    preg_match_all(self::$regEx['config'], $param, $args, PREG_PATTERN_ORDER);
201
    foreach ($args[1] as $arg) {
202
      if (array_key_exists($arg, $config)) {
203
        $value = $config[$arg];
204
        if (is_array($value)) {
205
          $config = $value;
206
        } else {
207
          break;
208
        }
209
      }
210
    }
211
    return $value;
212
  }
213
214
  /**
215
   * Retrieve param value from array in Fixtures
216
   *
217
   * @param string $param
218
   *
219
   * @return \mixed|null Returns parameter's value if exists, else null
220
   */
221
  //TODO: pass param ref to function (&) [performance]
222
  final protected function getValueFromArray(string $param)
223
  {
224
    $value = null;
225
226
    preg_match_all(self::$regEx['array'], $param, $args);
227
    $array = Fixtures::get($args['var'][0]);
228
    if (array_key_exists($args['key'][0], $array)) {
229
        $value = $array[$args['key'][0]];
230
    }
231
    return $value;
232
  }
233
234
  /**
235
   * Capture suite's config before any execution
236
   *
237
   * @param array $settings
238
   * @return void
239
   *
240
   * @codeCoverageIgnore
241
   * @ignore Codeception specific
242
   */
243
  final public function _beforeSuite($settings = [])
244
  {
245
    self::$suiteConfig = $settings;
246
  }
247
248
  /**
249
   * Parse scenario's step before execution
250
   *
251
   * @param \Codeception\Step $step
252
   * @return void
253
   */
254
  final public function _beforeStep(Step $step)
255
  {
256
    // access to the protected property using reflection
257
    $refArgs = new ReflectionProperty(get_class($step), 'arguments');
258
    // change property accessibility to public
259
    $refArgs->setAccessible(true);
260
    // retrieve 'arguments' value
261
    $args = $refArgs->getValue($step);
262
    foreach ($args as $index => $arg) {
263
      if (is_string($arg)) {
264
      // case if arg is a string
265
      // e.g. I see "{{param}}"
266
        $args[$index] = $this->getValueFromParam($arg);
267
      } elseif (is_a($arg, '\Behat\Gherkin\Node\TableNode')) {
268
      // case if arg is a table
269
      // e.g. I see :
270
      //  | paramater |
271
      //  | {{param}} |
272
        $prop = new ReflectionProperty(get_class($arg), 'table');
273
        $prop->setAccessible(true);
274
        $table = $prop->getValue($arg);
275
        foreach($table as $i => $row) {
276
          foreach ($row as $j => $cell) {
277
            $val = $this->getValueFromParam($cell);
278
            $table[$i][$j] = $val ? $val : null; // issue TableNode does not support `null` values in table
279
          }
280
        }
281
        $prop->setValue($arg, $table);
282
        $prop->setAccessible(false);
283
        $args[$index] = $arg;
284
      } elseif (is_array($arg)) {
285
        foreach ($arg as $k => $v) {
286
          if (is_string($v)) {
287
             $args[$index][$k] = $this->getValueFromParam($v);
288
          }
289
        }
290
      }
291
    }
292
    // set new arguments value
293
    $refArgs->setValue($step, $args);
294
  }
295
296
}
297