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\Reader;
13 use Behat\Behat\Context\Annotation\AnnotationReader;
14 use Behat\Behat\Context\Environment\ContextEnvironment;
15 use Behat\Testwork\Call\Callee;
17 use ReflectionException;
21 * Reads context callees by annotations using registered annotation readers.
23 * @author Konstantin Kudryashov <ever.zet@gmail.com>
25 final class AnnotatedContextReader implements ContextReader
27 const DOCLINE_TRIMMER_REGEX = '/^\/\*\*\s*|^\s*\*\s*|\s*\*\/$|\s*$/';
32 private static $ignoreAnnotations = array(
41 * @var AnnotationReader[]
43 private $readers = array();
46 * Registers annotation reader.
48 * @param AnnotationReader $reader
50 public function registerAnnotationReader(AnnotationReader $reader)
52 $this->readers[] = $reader;
58 public function readContextCallees(ContextEnvironment $environment, $contextClass)
60 $reflection = new ReflectionClass($contextClass);
63 foreach ($reflection->getMethods(ReflectionMethod::IS_PUBLIC) as $method) {
64 foreach ($this->readMethodCallees($reflection->getName(), $method) as $callee) {
73 * Loads callees associated with specific method.
75 * @param string $class
76 * @param ReflectionMethod $method
80 private function readMethodCallees($class, ReflectionMethod $method)
84 // read parent annotations
86 $prototype = $method->getPrototype();
87 // error occurs on every second PHP stable release - getPrototype() returns itself
88 if ($prototype->getDeclaringClass()->getName() !== $method->getDeclaringClass()->getName()) {
89 $callees = array_merge($callees, $this->readMethodCallees($class, $prototype));
91 } catch (ReflectionException $e) {
94 if ($docBlock = $method->getDocComment()) {
95 $callees = array_merge($callees, $this->readDocBlockCallees($class, $method, $docBlock));
102 * Reads callees from the method doc block.
104 * @param string $class
105 * @param ReflectionMethod $method
106 * @param string $docBlock
110 private function readDocBlockCallees($class, ReflectionMethod $method, $docBlock)
113 $description = $this->readDescription($docBlock);
114 $docBlock = $this->mergeMultilines($docBlock);
116 foreach (explode("\n", $docBlock) as $docLine) {
117 $docLine = preg_replace(self::DOCLINE_TRIMMER_REGEX, '', $docLine);
119 if ($this->isEmpty($docLine)) {
123 if ($this->isNotAnnotation($docLine)) {
127 if ($callee = $this->readDocLineCallee($class, $method, $docLine, $description)) {
128 $callees[] = $callee;
136 * Merges multiline strings (strings ending with "\")
138 * @param string $docBlock
142 private function mergeMultilines($docBlock)
144 return preg_replace("#\\\\$\s*+\*\s*+([^\\\\$]++)#m", '$1', $docBlock);
148 * Extracts a description from the provided docblock,
149 * with support for multiline descriptions.
151 * @param string $docBlock
155 private function readDescription($docBlock)
157 // Remove indentation
158 $description = preg_replace('/^[\s\t]*/m', '', $docBlock);
160 // Remove block comment syntax
161 $description = preg_replace('/^\/\*\*\s*|^\s*\*\s|^\s*\*\/$/m', '', $description);
163 // Remove annotations
164 $description = preg_replace('/^@.*$/m', '', $description);
166 // Ignore docs after a "--" separator
167 if (preg_match('/^--.*$/m', $description)) {
168 $descriptionParts = preg_split('/^--.*$/m', $description);
169 $description = array_shift($descriptionParts);
172 // Trim leading and trailing newlines
173 $description = trim($description, "\r\n");
179 * Checks if provided doc lien is empty.
181 * @param string $docLine
185 private function isEmpty($docLine)
187 return '' == $docLine;
191 * Checks if provided doc line is not an annotation.
193 * @param string $docLine
197 private function isNotAnnotation($docLine)
199 return '@' !== substr($docLine, 0, 1);
203 * Reads callee from provided doc line using registered annotation readers.
205 * @param string $class
206 * @param ReflectionMethod $method
207 * @param string $docLine
208 * @param null|string $description
210 * @return null|Callee
212 private function readDocLineCallee($class, ReflectionMethod $method, $docLine, $description = null)
214 if ($this->isIgnoredAnnotation($docLine)) {
218 foreach ($this->readers as $reader) {
219 if ($callee = $reader->readCallee($class, $method, $docLine, $description)) {
228 * Checks if provided doc line is one of the ignored annotations.
230 * @param string $docLine
234 private function isIgnoredAnnotation($docLine)
236 $lowDocLine = strtolower($docLine);
237 foreach (self::$ignoreAnnotations as $ignoredAnnotation) {
238 if ($ignoredAnnotation == substr($lowDocLine, 0, strlen($ignoredAnnotation))) {