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

GherkinParam::_beforeSuite()   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 1
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
          $variable = trim(preg_filter(self::$regEx['filter'], '', $variable));
106
          // config case
107
          if (preg_match(self::$regEx['config'], $variable)) {
108
            $values[] = $this->getValueFromConfig($variable);
109
          } 
110
          // array case
111
          elseif (preg_match(self::$regEx['array'], $variable)) {
112
            try {
113
              $values[] = $this->getValueFromArray($variable);
114
            } catch(RuntimeException $e) {
115
              if ($this->throwException) throw new GherkinParamException();
116
              if ($this->nullable) $values[] = null;
117
            }
118
          } 
119
          // normal case
120
          else {
121
            try {
122
              $values[] = Fixtures::get($variable);
123
            } catch(RuntimeException $e) {
124
              if ($this->throwException) throw new GherkinParamException();
125
              if ($this->nullable) $values[] = null;
126
            }
127
          }
128
          // if machting value return is not found (null)
129
          if (is_null(end($values))) {
130
            if ($this->throwException) throw new GherkinParamException();
131
          }
132
        }
133
134
        // array str_replace cannot be used 
135
        // due to the default behavior when `search` and `replace` arrays size mismatch
136
        $param = $this->mapParametersToValues($matches, $values, $param);
137
138
      } catch(GherkinParamException $e) {
139
        // only active if throwException setting is true
140
        throw new ExtensionException(
141
          $this, 
142
          "Incorrect parameter name ${param}, or not initialized"
143
        );
144
      }
145
    
146
    }
147
148
    return $param;
149
  }
150
151
  /**
152
   * Replace parameters' matches by corresponding values
153
   *
154
   * @param array $matches
155
   * @param array $values
156
   * @param string $param
157
   *
158
   * @return \mixed|null Returns parameter's value if exists, else parameter {{name}}
159
   */  
160
  //TODO: pass param ref to function (&) [performance]
161
  final private function mapParametersToValues(array $matches, array $values, string $param)
162
  {
163
    //TODO: move count() into separate variable [performance]
164
    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...
165
      $search = $matches[$i];
166
      if (isset($values[$i])) {
167
        $replacement = $values[$i];
168
        if (\is_array($replacement)) { 
169
          // case of replacement is an array (case of config param), ie param does not exists
170
          if ($this->throwException) throw new GherkinParamException();
171
          if ($this->nullable) $param = null;
172
          break;
173
        }
174
        //TODO: replace str_replace by strtr (performance)
175
        $param = \str_replace($search, $replacement, $param);
176
      } else {
177
        if ($this->throwException) throw new GherkinParamException();
178
        if ($this->nullable) $param = null;
179
      }
180
    }
181
    return $param;
182
  }
183
184
  /**
185
   * Retrieve param value from current suite config
186
   *
187
   * @param string $param
188
   *
189
   * @return \mixed|null Returns parameter's value if exists, else null
190
   */
191
  //TODO: pass param ref to function (&) [performance]
192
  final protected function getValueFromConfig(string $param)
193
  {
194
    $value = null;
195
    $config = self::$suiteConfig;
196
197
    preg_match_all(self::$regEx['config'], $param, $args, PREG_PATTERN_ORDER);
198
    foreach ($args[1] as $arg) {
199
      if (array_key_exists($arg, $config)) {
200
        $value = $config[$arg];
201
        if (is_array($value)) {
202
          $config = $value;
203
        } else {
204
          break;
205
        }
206
      }
207
    }
208
    return $value;
209
  }
210
211
  /**
212
   * Retrieve param value from array in Fixtures
213
   *
214
   * @param string $param
215
   *
216
   * @return \mixed|null Returns parameter's value if exists, else null
217
   */
218
  //TODO: pass param ref to function (&) [performance]
219
  final protected function getValueFromArray(string $param)
220
  {
221
    $value = null;
222
223
    preg_match_all(self::$regEx['array'], $param, $args);
224
    $array = Fixtures::get($args['var'][0]);
225
    if (array_key_exists($args['key'][0], $array)) {
226
      $value = $array[$args['key'][0]];
227
    }
228
    return $value;
229
  }
230
231
  /**
232
   * Capture suite's config before any execution
233
   *
234
   * @param array $settings
235
   * @return void
236
   *
237
   * @codeCoverageIgnore
238
   * @ignore Codeception specific
239
   */
240
  final public function _beforeSuite($settings = [])
241
  {
242
    self::$suiteConfig = $settings;
243
  }
244
245
  /**
246
   * Parse scenario's step before execution
247
   *
248
   * @param \Codeception\Step $step
249
   * @return void
250
   */
251
  final public function _beforeStep(Step $step)
252
  {
253
    // access to the protected property using reflection
254
    $refArgs = new ReflectionProperty(get_class($step), 'arguments');
255
    // change property accessibility to public
256
    $refArgs->setAccessible(true);
257
    // retrieve 'arguments' value
258
    $args = $refArgs->getValue($step);
259
    foreach ($args as $index => $arg) {
260
      if (is_string($arg)) {
261
      // case if arg is a string
262
      // e.g. I see "{{param}}"
263
        $args[$index] = $this->getValueFromParam($arg);
264
      } elseif (is_a($arg, '\Behat\Gherkin\Node\TableNode')) {
265
      // case if arg is a table
266
      // e.g. I see :
267
      //  | paramater |
268
      //  | {{param}} |
269
        $prop = new ReflectionProperty(get_class($arg), 'table');
270
        $prop->setAccessible(true);
271
        $table = $prop->getValue($arg);
272
        foreach($table as $i => $row) {
273
          foreach ($row as $j => $cell) {
274
            $val = $this->getValueFromParam($cell);
275
            $table[$i][$j] = $val ? $val : null; // issue TableNode does not support `null` values in table
276
          }
277
        }
278
        $prop->setValue($arg, $table);
279
        $prop->setAccessible(false);
280
        $args[$index] = $arg;
281
      } elseif (is_array($arg)) {
282
        foreach ($arg as $k => $v) {
283
          if (is_string($v)) {
284
            $args[$index][$k] = $this->getValueFromParam($v);
285
          }
286
        }
287
      }
288
    }
289
    // set new arguments value
290
    $refArgs->setValue($step, $args);
291
  }
292
293
}
294