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