function it_does_not_take_a_screenshot_after_a_skipped_step(ScreenshotTaker $screenshotTaker, Environment $env, FeatureNode $feature, StepNode $step, StepResult $result, Teardown $tearDown) { $event = new AfterStepTested($env->getWrappedObject(), $feature->getWrappedObject(), $step->getWrappedObject(), $result->getWrappedObject(), $tearDown->getWrappedObject()); $result->getResultCode()->willReturn(TestResult::SKIPPED); $screenshotTaker->takeScreenshot()->shouldNotBeCalled(); $this->takeScreenshot($event); }
public function addSnippet(StepNode $step) { $args = []; $pattern = $step->getText(); // match numbers (not in quotes) if (preg_match_all('~([\\d\\.])(?=([^"]*"[^"]*")*[^"]*$)~', $pattern, $matches)) { foreach ($matches[1] as $num => $param) { $num++; $args[] = '$num' . $num; $pattern = str_replace($param, ":num{$num}", $pattern); } } // match quoted string if (preg_match_all('~"(.*?)"~', $pattern, $matches)) { foreach ($matches[1] as $num => $param) { $num++; $args[] = '$arg' . $num; $pattern = str_replace('"' . $param . '"', ":arg{$num}", $pattern); } } if (in_array($pattern, $this->processed)) { return; } $methodName = preg_replace('~(\\s+?|\'|\\"|\\W)~', '', ucwords(preg_replace('~"(.*?)"|\\d+~', '', $step->getText()))); $this->snippets[] = (new Template($this->template))->place('type', $step->getKeywordType())->place('text', $pattern)->place('methodName', lcfirst($methodName))->place('params', implode(', ', $args))->produce(); $this->processed[] = $pattern; }
/** * Checks if Feature matches specified filter. * * @param StepNode $step * @param string $filterString * * @return Boolean */ private function isStepMatch(StepNode $step, $filterString) { if ('/' === $filterString[0]) { return 1 === preg_match($filterString, $step->getText()); } return false !== mb_strpos($step->getText(), $filterString, 0, 'utf8'); }
/** * Initializes definition snippet. * * @param Behat\Gherkin\Node\StepNode $step step interested in snippet * @param string $template definition snippet template */ public function __construct(StepNode $step, $template) { $type = $step->getType(); $this->type = in_array($type, array('Given', 'When', 'Then')) ? $type : 'Given'; $this->template = $template; $this->steps[] = $step; }
/** * Loads definitions and translations from provided context. * * @param ContextInterface $context * @param StepNode $step * * @return DefinitionSnippet */ public function propose(ContextInterface $context, StepNode $step) { $text = $step->getText(); $regex = preg_replace('/([\\/\\[\\]\\(\\)\\\\^\\$\\.\\|\\?\\*\\+\'])/', '\\\\$1', $text); $regex = preg_replace(array("/(?<= |^)\\\\'(?:((?!\\').)*)\\\\'(?= |\$)/", '/(?<= |^)\\"(?:[^\\"]*)\\"(?= |$)/', '/(\\d+)/'), array("\\'([^\\']*)\\'", "\"([^\"]*)\"", "(\\d+)"), $regex); preg_match('/' . $regex . '/', $text, $matches); $count = count($matches) - 1; $args = array("\$world"); for ($i = 0; $i < $count; $i++) { $args[] = "\$arg" . ($i + 1); } foreach ($step->getArguments() as $argument) { if ($argument instanceof PyStringNode) { $args[] = "\$string"; } elseif ($argument instanceof TableNode) { $args[] = "\$table"; } } $description = sprintf(<<<PHP \$steps->%s('/^%s\$/', function(%s) { throw new \\Behat\\Behat\\Exception\\PendingException(); }); PHP , '%s', $regex, implode(', ', $args)); return new DefinitionSnippet($step, $description); }
/** * Defines a step with ->Given|When|Then|...('/regex/', callback) or * call a step with ->Given|When|Then|...('I enter "12" in the field', $world) or * even with arguments ->Given|When|Then|...('I fill up fields', $world, $table). * * @param string $type step type (Given|When|Then|...) * @param string $arguments step regex & callback * * @return ClosuredDefinitionLoader * * @throws RedundantException if step definition is already exists */ public function __call($type, $arguments) { if (2 == count($arguments) && is_callable($arguments[1])) { switch (strtolower($type)) { case 'when': $definition = new When($arguments[1], $arguments[0]); break; case 'then': $definition = new Then($arguments[1], $arguments[0]); break; case 'given': default: $definition = new Given($arguments[1], $arguments[0]); break; } $this->dispatcher->addDefinition($definition); } else { $text = array_shift($arguments); $world = array_shift($arguments); $step = new StepNode($type, $text); $step->setArguments($arguments); $definition = $this->dispatcher->findDefinition($world, $step); $definition->run($world); } return $this; }
public function testLine() { $step = new StepNode('Given'); $this->assertEquals(0, $step->getLine()); $step = new StepNode('Given', null, 23); $this->assertEquals(23, $step->getLine()); }
/** * Initializes Then substep. */ public function __construct() { $arguments = func_get_args(); $text = array_shift($arguments); $node = new StepNode('Then', $text); $node->setArguments($arguments); $this->node = $node; }
/** * {@inheritdoc} */ public function printStep(Formatter $formatter, Scenario $scenario, StepNode $step, StepResult $result) { $this->printText($formatter->getOutputPrinter(), $step->getKeyword(), $step->getText(), $result); $this->pathPrinter->printStepPath($formatter, $scenario, $step, $result, mb_strlen($this->indentText, 'utf8')); $this->printArguments($formatter, $step->getArguments(), $result); $this->printStdOut($formatter->getOutputPrinter(), $result); $this->printException($formatter->getOutputPrinter(), $result); }
function it_generates_filename_from_step_name(ScreenshotTaker $screenshotTaker, StepFilenameGenerator $filenameGenerator, Environment $env, FeatureNode $feature, StepNode $step, StepResult $result, Teardown $tearDown) { $event = new AfterStepTested($env->getWrappedObject(), $feature->getWrappedObject(), $step->getWrappedObject(), $result->getWrappedObject(), $tearDown->getWrappedObject()); $result->getResultCode()->willReturn(TestResult::FAILED); $filenameGenerator->convertStepToFileName($step)->willReturn('test.jpg')->shouldBeCalled(); $screenshotTaker->takeScreenshot('test.jpg')->shouldBeCalled(); $this->checkAfterStep($event); }
/** * Loads definitions and translations from provided context. * * @param ContextInterface $context * @param StepNode $step * * @return DefinitionSnippet */ public function propose(ContextInterface $context, StepNode $step) { $contextRefl = new \ReflectionObject($context); $contextClass = $contextRefl->getName(); $replacePatterns = array("/(?<= |^)\\\\'(?:((?!\\').)*)\\\\'(?= |\$)/", '/(?<= |^)\\"(?:[^\\"]*)\\"(?= |$)/', '/(\\d+)/'); $text = $step->getText(); $text = preg_replace('/([\\/\\[\\]\\(\\)\\\\^\\$\\.\\|\\?\\*\\+\'])/', '\\\\$1', $text); $regex = preg_replace($replacePatterns, array("\\'([^\\']*)\\'", "\"([^\"]*)\"", "(\\d+)"), $text); preg_match('/' . $regex . '/', $step->getText(), $matches); $count = count($matches) - 1; $methodName = preg_replace($replacePatterns, '', $text); $methodName = Transliterator::transliterate($methodName, ' '); $methodName = preg_replace('/[^a-zA-Z\\_\\ ]/', '', $methodName); $methodName = str_replace(' ', '', ucwords($methodName)); if (0 !== strlen($methodName)) { $methodName[0] = strtolower($methodName[0]); } else { $methodName = 'stepDefinition1'; } // get method number from method name $methodNumber = 2; if (preg_match('/(\\d+)$/', $methodName, $matches)) { $methodNumber = intval($matches[1]); } // check that proposed method name isn't arelady defined in context while ($contextRefl->hasMethod($methodName)) { $methodName = preg_replace('/\\d+$/', '', $methodName); $methodName .= $methodNumber++; } // check that proposed method name haven't been proposed earlier if (isset(self::$proposedMethods[$contextClass])) { foreach (self::$proposedMethods[$contextClass] as $proposedRegex => $proposedMethod) { if ($proposedRegex !== $regex) { while ($proposedMethod === $methodName) { $methodName = preg_replace('/\\d+$/', '', $methodName); $methodName .= $methodNumber++; } } } } self::$proposedMethods[$contextClass][$regex] = $methodName; $args = array(); for ($i = 0; $i < $count; $i++) { $args[] = "\$arg" . ($i + 1); } foreach ($step->getArguments() as $argument) { if ($argument instanceof PyStringNode) { $args[] = "PyStringNode \$string"; } elseif ($argument instanceof TableNode) { $args[] = "TableNode \$table"; } } $description = $this->generateSnippet($regex, $methodName, $args); return new DefinitionSnippet($step, $description); }
public function addSnippet(StepNode $step) { $args = []; $pattern = $step->getText(); if (preg_match_all('~"(.*?)"~', $pattern, $matches)) { foreach ($matches[1] as $num => $param) { $num++; $args[] = '$arg' . $num; $pattern = str_replace('"' . $param . '"', ":arg{$num}", $pattern); } } $methodName = preg_replace('~(\\s+?|\'|\\")~', '', ucwords(preg_replace('~"(.*?)"~', '', $step->getText()))); $this->snippets[] = (new Template($this->template))->place('type', $step->getKeywordType())->place('text', $pattern)->place('methodName', lcfirst($methodName))->place('params', implode(', ', $args))->produce(); }
/** * Defines a step with ->Given|When|Then|...('/regex/', callback) or * call a step with ->Given|When|Then|...('I enter "12" in the field', $world) or * even with arguments ->Given|When|Then|...('I fill up fields', $world, $table). * * @param string $type step type (Given|When|Then|...) * @param string $arguments step regex & callback * * @throws Behat\Behat\Exception\Redundant if step definition is already exists */ public function __call($type, $arguments) { if (2 == count($arguments) && is_callable($arguments[1])) { $debug = debug_backtrace(); $debug = $debug[1]; $this->objects[] = new Definition($type, $arguments[0], $arguments[1], $debug['file'], $debug['line']); } else { $text = array_shift($arguments); $world = array_shift($arguments); $step = new StepNode($type, $text); $step->setArguments($arguments); $definition = $this->dispatcher->findDefinition($step); $definition->run($world); } return $this; }
protected function runStep(StepNode $stepNode) { $params = []; if ($stepNode->hasArguments()) { $args = $stepNode->getArguments(); $table = $args[0]; if ($table instanceof TableNode) { $params = [$table->getTableAsString()]; } } $meta = new Meta($stepNode->getText(), $params); $meta->setPrefix($stepNode->getKeyword()); $this->scenario->setMetaStep($meta); // enable metastep $stepText = $stepNode->getText(); $this->getScenario()->comment(null); // make metastep to be printed even if no steps foreach ($this->steps as $pattern => $context) { $matches = []; if (!preg_match($pattern, $stepText, $matches)) { continue; } array_shift($matches); if ($stepNode->hasArguments()) { $matches = array_merge($matches, $stepNode->getArguments()); } call_user_func_array($context, $matches); // execute the step break; } $this->scenario->setMetaStep(null); // disable metastep }
/** * @param StepNode $step * * @return string */ private function getArguments(StepNode $step) { if (!$step->hasArguments()) { return; } return implode(array_map(function (ArgumentInterface $argument) { if (in_array($argument->getNodeType(), ['Table', 'ExampleTable'])) { return implode(array_map(function ($arguments) { return $this->indent(self::INDENTATION * 2 + 4) . trim($arguments) . "\n"; }, explode("\n", $argument->getTableAsString()))); } if ('PyString' === $argument->getNodeType()) { return $this->encapsulateAsPyString(implode(array_map(function ($arguments) { return rtrim($this->indent(self::INDENTATION * 2 + 2) . trim($arguments)) . "\n"; }, $argument->getStrings()))); } }, $step->getArguments())); }
/** * {@inheritdoc} * * @throws AmbiguousMatchException */ public function searchDefinition(Environment $environment, FeatureNode $feature, StepNode $step) { $suite = $environment->getSuite(); $language = $feature->getLanguage(); $stepText = $step->getText(); $multi = $step->getArguments(); $definitions = array(); $result = null; foreach ($this->repository->getEnvironmentDefinitions($environment) as $definition) { $definition = $this->translator->translateDefinition($suite, $definition, $language); if (!($newResult = $this->match($definition, $stepText, $multi))) { continue; } $result = $newResult; $definitions[] = $newResult->getMatchedDefinition(); } if (count($definitions) > 1) { throw new AmbiguousMatchException($result->getMatchedText(), $definitions); } return $result; }
/** * Prints step using provided printer. * * @param Formatter $formatter * @param Scenario $scenario * @param StepNode $step * @param StepResult $result */ public function printStep(Formatter $formatter, Scenario $scenario, StepNode $step, StepResult $result) { /** @var JUnitOutputPrinter $outputPrinter */ $outputPrinter = $formatter->getOutputPrinter(); $message = $step->getKeyword() . ' ' . $step->getText(); if ($result instanceof ExceptionResult && $result->hasException()) { $message .= ': ' . $this->exceptionPresenter->presentException($result->getException()); } $attributes = array('message' => $message); switch ($result->getResultCode()) { case TestResult::FAILED: $outputPrinter->addTestcaseChild('failure', $attributes); break; case TestResult::PENDING: $attributes['type'] = 'pending'; $outputPrinter->addTestcaseChild('error', $attributes); break; case StepResult::UNDEFINED: $attributes['type'] = 'undefined'; $outputPrinter->addTestcaseChild('error', $attributes); break; } }
/** * Prints step definition path. * * @param StepNode $step step node * @param DefinitionInterface $definition definition (if found one) * * @uses printPathComment() */ protected function printStepDefinitionPath(StepNode $step, DefinitionInterface $definition) { if ($this->getParameter('paths')) { $type = $step->getType(); $text = $this->inOutlineSteps ? $step->getCleanText() : $step->getText(); $indent = $this->stepIndent; $nameLength = mb_strlen("{$indent}{$type} {$text}"); $indentCount = $nameLength > $this->maxLineLength ? 0 : $this->maxLineLength - $nameLength; $this->printPathComment($this->relativizePathsInString($definition->getPath()), $indentCount); if ($this->getParameter('expand')) { $this->maxLineLength = max($this->maxLineLength, $nameLength); } } else { $this->writeln(); } }
/** * Dumps a step. * * @param StepNode $step Step node instance * * @return string * * @throws Exception if invalid step type providen */ public function dumpStep(StepNode $step) { switch ($step->getType()) { case 'Given': $kw = $this->keywords->getGivenKeywords(); break; case 'When': $kw = $this->keywords->getWhenKeywords(); break; case 'Then': $kw = $this->keywords->getThenKeywords(); break; case 'But': $kw = $this->keywords->getButKeywords(); break; case 'And': $kw = $this->keywords->getAndKeywords(); break; default: throw new Exception("invalid type given : " . $step->getType()); } return $this->dumpText($kw . ' ' . $step->getText()); }
/** * Finds step definition, that match specified step. * * @param Behat\Gherkin\Node\StepNode $step found step * * @return Behat\Behat\Definition\Definition * * @uses loadDefinitions() * * @throws Behat\Behat\Exception\Ambiguous if step description is ambiguous * @throws Behat\Behat\Exception\Undefined if step definition not found */ public function findDefinition(StepNode $step) { if (!count($this->definitions)) { $this->loadDefinitions(); } $text = $step->getText(); $multiline = $step->getArguments(); $matches = array(); // find step to match foreach ($this->definitions as $origRegex => $definition) { $transRegex = $this->translateDefinitionRegex($origRegex, $step->getLanguage()); if (preg_match($origRegex, $text, $arguments) || $origRegex !== $transRegex && preg_match($transRegex, $text, $arguments)) { // prepare callback arguments $arguments = $this->prepareCallbackArguments($definition->getCallbackReflection(), array_slice($arguments, 1), $multiline); // transform arguments foreach ($arguments as $num => $argument) { foreach ($this->transformations as $transformation) { if ($newArgument = $transformation->transform($argument)) { $arguments[$num] = $newArgument; } } } // set matched definition $definition->setMatchedText($text); $definition->setValues($arguments); $matches[] = $definition; } } if (count($matches) > 1) { throw new Ambiguous($text, $matches); } if (0 === count($matches)) { throw new Undefined($text); } return $matches[0]; }
/** * Prints path to step. * * @param Behat\Gherkin\Node\StepNode $step step node * @param Behat\Behat\Definition\Definition $definition definition (if step defined) * @param Exception $exception exception (if step failed) */ protected function printStepPath(StepNode $step, Definition $definition = null, \Exception $exception = null) { $color = $exception instanceof Pending ? 'pending' : 'failed'; $type = $step->getType(); $text = $step->getText(); $stepPath = "In step `{$type} {$text}'."; $stepPathLn = mb_strlen($stepPath); $node = $step->getParent(); if ($node instanceof BackgroundNode) { $scenarioPath = "From scenario background."; } else { $title = $node->getTitle(); $title = $title ? "`{$title}'" : '***'; $scenarioPath = "From scenario {$title}."; } $scenarioPathLn = mb_strlen($scenarioPath); $this->maxLineLength = max($this->maxLineLength, $stepPathLn); $this->maxLineLength = max($this->maxLineLength, $scenarioPathLn); $this->write(" {+{$color}}{$stepPath}{-{$color}}"); if (null !== $definition) { $indentCount = $this->maxLineLength - $stepPathLn; $this->printPathComment($definition->getFile(), $definition->getLine(), $indentCount); } else { $this->writeln(); } $this->write(" {+{$color}}{$scenarioPath}{-{$color}}"); $indentCount = $this->maxLineLength - $scenarioPathLn; $this->printPathComment($node->getFile(), $node->getLine(), $indentCount); $this->writeln(); }
/** * Returns an array of classes used by the snippet template * * @param StepNode $step * * @return string[] */ private function getUsedClasses(StepNode $step) { $usedClasses = array('Behat\\Behat\\Tester\\Exception\\PendingException'); foreach ($step->getArguments() as $argument) { if ($argument instanceof TableNode) { $usedClasses[] = 'Behat\\Gherkin\\Node\\TableNode'; } elseif ($argument instanceof PyStringNode) { $usedClasses[] = 'Behat\\Gherkin\\Node\\PyStringNode'; } } return $usedClasses; }
/** * Finds step definition, that match specified step. * * @param ContextInterface $context * @param StepNode $step * @param bool $skip * * @return Definition * * @uses loadDefinitions() * * @throws AmbiguousException if step description is ambiguous * @throws UndefinedException if step definition not found */ public function findDefinition(ContextInterface $context, StepNode $step, $skip = false) { $text = $step->getText(); $multiline = $step->getArguments(); $matches = array(); // find step to match foreach ($this->getDefinitions() as $origRegex => $definition) { $transRegex = $this->translateDefinitionRegex($origRegex, $step->getLanguage()); // if not regex really (string) - transform into it if (0 !== strpos($origRegex, '/')) { $origRegex = '/^' . preg_quote($origRegex, '/') . '$/'; $transRegex = '/^' . preg_quote($transRegex, '/') . '$/'; } if (preg_match($origRegex, $text, $arguments) || $origRegex !== $transRegex && preg_match($transRegex, $text, $arguments)) { // prepare callback arguments $arguments = $this->prepareCallbackArguments($context, $definition->getCallbackReflection(), array_slice($arguments, 1), $multiline); if (!$skip) { // transform arguments foreach ($arguments as &$argument) { foreach ($this->getTransformations() as $trans) { $transRegex = $this->translateDefinitionRegex($trans->getRegex(), $step->getLanguage()); $newArgument = $trans->transform($transRegex, $context, $argument); if (null !== $newArgument) { $argument = $newArgument; } } } } // set matched definition $definition->setMatchedText($text); $definition->setValues($arguments); $matches[] = $definition; } } if (count($matches) > 1) { throw new AmbiguousException($text, $matches); } if (0 === count($matches)) { throw new UndefinedException($text); } return $matches[0]; }
/** * Changes step node type for types But, And to type of previous step if it exists else sets to Given * * @param StepNode $node * @param StepNode[] $steps * @return StepNode */ private function normalizeStepNodeKeywordType(StepNode $node, array $steps = array()) { if (in_array($node->getKeywordType(), array('And', 'But'))) { if ($prev = end($steps)) { $keywordType = $prev->getKeywordType(); } else { $keywordType = 'Given'; } $node = new StepNode($node->getKeyword(), $node->getText(), $node->getArguments(), $node->getLine(), $keywordType); } return $node; }
/** * @param FeatureNode $featureNode * @param StepNode $stepNode * @return ScenarioInterface|null */ private function detectScenario(FeatureNode $featureNode, StepNode $stepNode) { foreach ($featureNode->getScenarios() as $scenario) { foreach ($scenario->getSteps() as $step) { if ($step->getLine() === $stepNode->getLine() && $step->getText() === $stepNode->getText()) { return $scenario; } } } return null; }
/** * Executes steps chain (if there's one). * * Added code to trigger the BeforeStep and AfterStep hooks. * * @param StepNode $step step node * @param mixed $chain chain * * @throws \Exception */ protected function executeStepsChain(StepNode $step, $chain = null) { if (null === $chain) { return; } $chain = is_array($chain) ? $chain : array($chain); foreach ($chain as $chainItem) { if ($chainItem instanceof SubstepInterface) { $substepNode = $chainItem->getStepNode(); $substepNode->setParent($step->getParent()); $this->dispatcher->dispatch('beforeStep', new StepEvent($substepNode, $this->logicalParent, $this->context)); $substepEvent = $this->executeStep($substepNode); if (StepEvent::PASSED !== $substepEvent->getResult()) { throw $substepEvent->getException(); } if ($substepEvent->getLogicalParent()->getKeyword() != 'Scenario Outline') { $this->dispatcher->dispatch('afterStep', $substepEvent); } } elseif (is_callable($chainItem)) { $this->executeStepsChain($step, call_user_func($chainItem)); } } }
/** * Initializes sub-step. * * @param string $type * @param string $text * @param array $arguments */ public function __construct($type, $text, array $arguments) { parent::__construct('Given', $text, $arguments, 0); }
/** * {@inheritdoc} */ protected function printStepName(StepNode $step, DefinitionInterface $definition = null, $color) { $type = $step->getType(); $text = $this->inOutlineSteps ? $step->getCleanText() : $step->getText(); if (null !== $definition) { $text = $this->colorizeDefinitionArguments($text, $definition, $color); } $this->writeln('<span class="keyword">' . $type . ' </span>'); $this->writeln('<span class="text">' . $text . '</span>'); }
/** * Loads step from provided hash. * * @param array $hash Step hash * @param integer $line Step definition line * * @return StepNode */ protected function loadStepHash(array $hash, $line = 0) { $step = new Node\StepNode($hash['type'], isset($hash['text']) ? $hash['text'] : null, isset($hash['line']) ? $hash['line'] : $line); if (isset($hash['arguments'])) { foreach ($hash['arguments'] as $argumentHash) { if ('table' === $argumentHash['type']) { $step->addArgument($this->loadTableHash($argumentHash['rows'])); } elseif ('pystring' === $argumentHash['type']) { $step->addArgument($this->loadPyStringHash($argumentHash)); } } } return $step; }
/** * Executes steps chain (if there's one). * * Overwriten method to run behat hooks between chain steps. * * @param StepNode $step step node * @param mixed $chain chain * * @throws \Exception */ private function executeStepsChainWithHooks(StepNode $step, $chain = null) { if (null === $chain) { // If there are no more chained steps below we will dispatch the // after step event, skipping the step that looks for exceptions here. if (strstr($step->getText(), self::EXCEPTIONS_STEP_TEXT) === false) { $this->dispatchafterstep = true; } return; } $chain = is_array($chain) ? $chain : array($chain); foreach ($chain as $chainItem) { if ($chainItem instanceof SubstepInterface) { $substepNode = $chainItem->getStepNode(); $substepNode->setParent($step->getParent()); // Replace by tokens when needed. if ($substepNode->getParent() instanceof OutlineNode) { $substepNode = $substepNode->createExampleRowStep($this->tokens); } $this->dispatchafterstep = false; // Dispatch beforeStep event. $this->moodledispatcher->dispatch('beforeStep', new StepEvent($substepNode, $this->moodlelogicalParent, $this->moodlecontext)); $substepEvent = $this->executeStep($substepNode); // Dispatch afterStep event. if ($this->dispatchafterstep === true) { $this->moodledispatcher->dispatch('afterStep', $substepEvent); $this->dispatchafterstep = false; } // Here we mark the step as failed so parent steps in the chain // will not continue dispatching the afterStep event. if (StepEvent::PASSED !== $substepEvent->getResult()) { $this->failedstep = true; throw $substepEvent->getException(); } } elseif (is_callable($chainItem)) { $this->executeStepsChainWithHooks($step, call_user_func($chainItem)); } } }