public function enterNode(Node $node) { if (!$node instanceof Class_) { return; } if ($this->classes->exists($node)) { throw new Exception("Class {$node->name} already encountered"); } $properties = new SimpleOrderedMap(); foreach ($node->stmts as $stmt) { if (!$stmt instanceof Property) { continue; } foreach ($stmt->props as $prop) { if (!in_array($prop->name, $this->properties, true)) { continue; } if ($properties->exists($prop)) { throw new Exception("Property {$prop->name} already exists"); } if (!$prop->default instanceof Array_) { continue; } $symbols = self::arrayExtractItems($prop->default); if (empty($symbols)) { continue; } asort($symbols); $properties->add($prop, $symbols); } } $this->classes->add($node, $properties); }
/** * Rewrite the PHPDOC of a class to contain the documentation of the * special CakePHP2 magic properties. * * $classes maps parsed PHP classes to properties and each property contains * a mapping of injected property names to their class types * * This nodes/statement from PhpParser also contain additional information * about the PHPDOC and in which lines in the source those elements are. * This information is used to either create a new PHPDOC or change an * existing one to contain the magic properties. * * Note: it may seem redundant that the $classes->property->symbol mapping * already has all properties and a separate $properties list is required. * However during collecting a property we don't know yet which top level * class it is in, i.e. which properties apply to the current class. * This is solves here insofar the top level class check happened (outside) * already and thus we get a specific list of $properties to document. * * @param array $code The code of the PHP file as returned by the file() * command, i.e. an every with every line in the file * including EOL. * @param SimpleOrderedMap $classes Mapping of classes to the collected * property which maps to a list of symbols. * @param string[] $properties Name the properties whose symbols should be * written to PHPDOC. * @param bool $removeUnknownProperties Removes all other properties (first) * and then adds the found ones. This may * remove properties which have been * manually added so use with care. * @throws Exception * @throws SimpleOrderedMapException * @return array Returns the (possible) modified PHP source code. */ public static function apply(array $code, SimpleOrderedMap $classes, array $properties, $removeUnknownProperties = false) { if (empty($code)) { return $code; # nothing to do } $insertions = []; # record at which line what kind of insertions will happen /** @var Class_ $class */ foreach ($classes->keys() as $class) { $currentInsertions = 0; # keep count how many properties == lines we've added # Detect existing or create new PhpDoc and figure out indentation $phpDoc = self::getLastPhpDocComment($class); $phpDocNumLinesInSource = 0; $docIndent = ''; if ($phpDoc instanceof Doc) { # get indentation from existing phpdoc $text = $phpDoc->getText(); if (preg_match(self::RE_INDENT, $text, $m)) { $docIndent = $m['indent']; } # If it's a single line comment, we've to break it up if (!preg_match('/\\R/', $text)) { $textNoEndComment = preg_replace(';\\*/\\s*;', '', $text); # If there was no change to the text, we couldn't remove the end of # comment we expected to be there; that's rather unexpected if ($text === $textNoEndComment) { throw new Exception('Unable to remove end doc comment marker \'*/\' from single line comment'); } # Since we've add a new line we need to know the files EOL $eol = self::extractEol(reset($code)); $text = $textNoEndComment . $eol . $docIndent . ' */'; $phpDoc->setText($text); $lines = self::splitStringIntoLines($text); $phpDocNumLinesInSource = 1; # hardcoded because we know } else { $lines = self::splitStringIntoLines($text); $phpDocNumLinesInSource = count($lines); } if ($removeUnknownProperties) { # Remove every line already containing a @property statement foreach ($lines as $i => $line) { if (false !== strpos($line, '@property')) { unset($lines[$i]); } } $lines = array_values($lines); # ensure no gaps in indices $phpDoc->setText(join('', $lines)); } assert(0 !== $phpDocNumLinesInSource); } else { # No PHPDOC found, create a new one $default = self::DEFAULT_PHPDOC; # indent default phpdoc by current class indentation $classLine = $code[$class->getAttribute('startLine') - 1]; if (preg_match(self::RE_INDENT, $classLine, $m)) { $docIndent = $m['indent']; # now prepend the indent before each line $lines = preg_split("/(\\R)/", $default, -1, PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_NO_EMPTY); for ($lineNr = 0; $lineNr < count($lines) >> 1; $lineNr++) { # accommodate for extra EOL matches $lines[$lineNr << 1] = $docIndent . $lines[$lineNr << 1]; } $default = join('', $lines); unset($lines); } $phpDoc = new Doc($default, $class->getAttribute('startLine')); unset($default); } $docIndent .= ' '; # extra space for slash # Go through the parsed properties and add them to the phpdoc if they # can't be found $currentProperties = $classes->get($class); /** @var Property $property */ foreach ($currentProperties->keys() as $property) { if (!in_array($property->name, array_keys($properties))) { continue; } $symbols = $currentProperties->get($property); foreach ($symbols as $symbol => $type) { # Extract only class name part (i.e. throw away name of plugins) $symbol = preg_replace('/.*\\.([^\\.]+)$/', '$1', $symbol); $type = preg_replace('/.*\\.([^\\.]+)$/', '$1', $type); $type = $properties[$property->name]($type); if (self::addPropertyIfNotExists($phpDoc, $type, $symbol, $docIndent)) { $currentInsertions++; } } } if ($currentInsertions === 0) { # no lines added, nothing to do continue; } # Accumulate sum of previously replaced lines to get actual line number $alreadyAddedLines = array_reduce($insertions, function ($carry, $item) { return $carry + $item['replaceNumLines']; }, 0); $insertions[$phpDoc->getLine() - 1 + $alreadyAddedLines] = ['doc' => $phpDoc, 'replaceNumLines' => $phpDocNumLinesInSource]; } foreach ($insertions as $lineNr => $data) { /** @var Doc $phpDoc */ $phpDoc = $data['doc']; $text = $phpDoc->getText(); $lines = self::splitStringIntoLines($text); array_splice($code, $lineNr, $data['replaceNumLines'], $lines); } return $code; }