4 * This file is part of the Behat.
5 * (c) Konstantin Kudryashov <ever.zet@gmail.com>
7 * For the full copyright and license information, please view the LICENSE
8 * file that was distributed with this source code.
11 namespace Behat\Behat\Context\Snippet\Generator;
13 use Behat\Behat\Context\Environment\ContextEnvironment;
14 use Behat\Behat\Context\Snippet\ContextSnippet;
15 use Behat\Behat\Definition\Pattern\PatternTransformer;
16 use Behat\Behat\Snippet\Exception\EnvironmentSnippetGenerationException;
17 use Behat\Behat\Snippet\Generator\SnippetGenerator;
18 use Behat\Gherkin\Node\PyStringNode;
19 use Behat\Gherkin\Node\StepNode;
20 use Behat\Gherkin\Node\TableNode;
21 use Behat\Testwork\Environment\Environment;
25 * Generates snippets for a context class.
27 * @author Konstantin Kudryashov <ever.zet@gmail.com>
29 final class ContextSnippetGenerator implements SnippetGenerator
34 private static $proposedMethods = array();
38 private static $templateTemplate = <<<TPL
42 public function %s(%s)
44 throw new PendingException();
48 * @var PatternTransformer
50 private $patternTransformer;
52 * @var TargetContextIdentifier
54 private $contextIdentifier;
56 * @var PatternIdentifier
58 private $patternIdentifier;
61 * Initializes snippet generator.
63 * @param PatternTransformer $patternTransformer
65 public function __construct(PatternTransformer $patternTransformer)
67 $this->patternTransformer = $patternTransformer;
69 $this->setContextIdentifier(new FixedContextIdentifier(null));
70 $this->setPatternIdentifier(new FixedPatternIdentifier(null));
74 * Sets target context identifier.
76 * @param TargetContextIdentifier $identifier
78 public function setContextIdentifier(TargetContextIdentifier $identifier)
80 $this->contextIdentifier = new CachedContextIdentifier($identifier);
84 * Sets target pattern type identifier.
86 * @param PatternIdentifier $identifier
88 public function setPatternIdentifier(PatternIdentifier $identifier)
90 $this->patternIdentifier = $identifier;
96 public function supportsEnvironmentAndStep(Environment $environment, StepNode $step)
98 if (!$environment instanceof ContextEnvironment) {
102 if (!$environment->hasContexts()) {
106 return null !== $this->contextIdentifier->guessTargetContextClass($environment);
112 public function generateSnippet(Environment $environment, StepNode $step)
114 if (!$environment instanceof ContextEnvironment) {
115 throw new EnvironmentSnippetGenerationException(sprintf(
116 'ContextSnippetGenerator does not support `%s` environment.',
117 get_class($environment)
121 $contextClass = $this->contextIdentifier->guessTargetContextClass($environment);
122 $patternType = $this->patternIdentifier->guessPatternType($contextClass);
123 $stepText = $step->getText();
124 $pattern = $this->patternTransformer->generatePattern($patternType, $stepText);
126 $methodName = $this->getMethodName($contextClass, $pattern->getCanonicalText(), $pattern->getPattern());
127 $methodArguments = $this->getMethodArguments($step, $pattern->getPlaceholderCount());
128 $snippetTemplate = $this->getSnippetTemplate($pattern->getPattern(), $methodName, $methodArguments);
130 $usedClasses = $this->getUsedClasses($step);
132 return new ContextSnippet($step, $snippetTemplate, $contextClass, $usedClasses);
136 * Generates method name using step text and regex.
138 * @param string $contextClass
139 * @param string $canonicalText
140 * @param string $pattern
144 private function getMethodName($contextClass, $canonicalText, $pattern)
146 $methodName = $this->deduceMethodName($canonicalText);
147 $methodName = $this->getUniqueMethodName($contextClass, $pattern, $methodName);
153 * Returns an array of method argument names from step and token count.
155 * @param StepNode $step
156 * @param integer $tokenCount
160 private function getMethodArguments(StepNode $step, $tokenCount)
163 for ($i = 0; $i < $tokenCount; $i++) {
164 $args[] = '$arg' . ($i + 1);
167 foreach ($step->getArguments() as $argument) {
168 $args[] = $this->getMethodArgument($argument);
175 * Returns an array of classes used by the snippet template
177 * @param StepNode $step
181 private function getUsedClasses(StepNode $step)
183 $usedClasses = array('Behat\Behat\Tester\Exception\PendingException');
185 foreach ($step->getArguments() as $argument) {
186 if ($argument instanceof TableNode) {
187 $usedClasses[] = 'Behat\Gherkin\Node\TableNode';
188 } elseif ($argument instanceof PyStringNode) {
189 $usedClasses[] = 'Behat\Gherkin\Node\PyStringNode';
197 * Generates snippet template using regex, method name and arguments.
199 * @param string $pattern
200 * @param string $methodName
201 * @param string[] $methodArguments
205 private function getSnippetTemplate($pattern, $methodName, array $methodArguments)
208 self::$templateTemplate,
209 str_replace('%', '%%', $pattern),
211 implode(', ', $methodArguments)
216 * Generates definition method name based on the step text.
218 * @param string $canonicalText
222 private function deduceMethodName($canonicalText)
224 // check that method name is not empty
225 if (0 !== strlen($canonicalText)) {
226 $canonicalText[0] = strtolower($canonicalText[0]);
228 return $canonicalText;
231 return 'stepDefinition1';
235 * Ensures uniqueness of the method name in the context.
237 * @param string $contextClass
238 * @param string $stepPattern
239 * @param string $name
243 private function getUniqueMethodName($contextClass, $stepPattern, $name)
245 $reflection = new ReflectionClass($contextClass);
247 $number = $this->getMethodNumberFromTheMethodName($name);
248 list($name, $number) = $this->getMethodNameNotExistentInContext($reflection, $name, $number);
249 $name = $this->getMethodNameNotProposedEarlier($contextClass, $stepPattern, $name, $number);
255 * Tries to deduct method number from the provided method name.
257 * @param string $methodName
261 private function getMethodNumberFromTheMethodName($methodName)
264 if (preg_match('/(\d+)$/', $methodName, $matches)) {
265 $methodNumber = intval($matches[1]);
268 return $methodNumber;
272 * Tries to guess method name that is not yet defined in the context class.
274 * @param ReflectionClass $reflection
275 * @param string $methodName
276 * @param integer $methodNumber
280 private function getMethodNameNotExistentInContext(ReflectionClass $reflection, $methodName, $methodNumber)
282 while ($reflection->hasMethod($methodName)) {
283 $methodName = preg_replace('/\d+$/', '', $methodName);
284 $methodName .= $methodNumber++;
287 return array($methodName, $methodNumber);
291 * Tries to guess method name that is not yet proposed to the context class.
293 * @param string $contextClass
294 * @param string $stepPattern
295 * @param string $name
296 * @param integer $number
300 private function getMethodNameNotProposedEarlier($contextClass, $stepPattern, $name, $number)
302 foreach ($this->getAlreadyProposedMethods($contextClass) as $proposedPattern => $proposedMethod) {
303 if ($proposedPattern === $stepPattern) {
307 while ($proposedMethod === $name) {
308 $name = preg_replace('/\d+$/', '', $name);
313 $this->markMethodAsAlreadyProposed($contextClass, $stepPattern, $name);
319 * Returns already proposed method names.
321 * @param string $contextClass
325 private function getAlreadyProposedMethods($contextClass)
327 return isset(self::$proposedMethods[$contextClass]) ? self::$proposedMethods[$contextClass] : array();
331 * Marks method as proposed one.
333 * @param string $contextClass
334 * @param string $stepPattern
335 * @param string $methodName
337 private function markMethodAsAlreadyProposed($contextClass, $stepPattern, $methodName)
339 self::$proposedMethods[$contextClass][$stepPattern] = $methodName;
343 * Returns method argument.
345 * @param string $argument
349 private function getMethodArgument($argument)
351 $arg = '__unknown__';
352 if ($argument instanceof PyStringNode) {
353 $arg = 'PyStringNode $string';
354 } elseif ($argument instanceof TableNode) {
355 $arg = 'TableNode $table';