Skip to content

Instantly share code, notes, and snippets.

@maiermic
Last active November 9, 2018 17:35
Generate PHPDoc @method for magic getters/setters
<?php
// See https://youtrack.jetbrains.com/issue/WI-19869
// require all php files (load class declarations)
$entitiesDirectory = '/path/to/entities';
$files =
new CallbackFilterIterator(
new RecursiveIteratorIterator(
new RecursiveDirectoryIterator($entitiesDirectory), RecursiveIteratorIterator::SELF_FIRST),
function (SplFileInfo $current, $key, $iterator) {
return ($current->getExtension() === 'php');
});
/** @var SplFileInfo $info */
foreach ($files as $info) {
require_once $info->getPathname();
}
foreach (get_declared_classes() as $className) {
try {
$reflectionClass = new ReflectionClass($className);
} catch (ReflectionException $e) {
error_log("ReflectionException $className");
continue;
}
$methodNames = array_map(
function (ReflectionMethod $method) {
return $method->getName();
},
$reflectionClass->getMethods());
$methodAnnotations = [];
$reflectionProperties = $reflectionClass->getProperties();
foreach ($reflectionProperties as $reflectionProperty) {
if ($reflectionProperty->isStatic()) {
continue;
}
$propertyName = $reflectionProperty->getName();
$matches = [];
preg_match(
'/@var (\w+\\\\)/',
$reflectionProperty->getDocComment(),
$matches);
if (!isset($matches[1])) {
error_log("$className::$propertyName has no type annotation");
continue;
}
$propertyType = $matches[1];
$getterName = 'get' . ucfirst($propertyName);
$setterName = 'set' . ucfirst($propertyName);
if (!in_array($getterName, $methodNames, true)) {
$methodAnnotations[] = " * @method $propertyType $getterName()";
}
if (!in_array($setterName, $methodNames, true)) {
$methodAnnotations[] = " * @method void $setterName($propertyType \$$propertyName)";
}
}
$hasBeenReplaced = false;
$oldDocComment = $reflectionClass->getDocComment();
$newLineRegex = '(\r\n|\r|\n)';
$newDocComment =
preg_replace(
"/(\\s\\*\\s*$newLineRegex( \* @method.+?$newLineRegex)+)?\\s?\\*\\//",
" *\n" . join("\n", $methodAnnotations) . "\n */",
$oldDocComment,
1,
$hasBeenReplaced);
if (!$hasBeenReplaced) {
error_log("Could not update $className");
} elseif ($oldDocComment === $newDocComment) {
error_log("Nothing to update in $className");
} else {
file_put_contents(
$reflectionClass->getFileName(),
str_replace(
$oldDocComment,
$newDocComment,
file_get_contents($reflectionClass->getFileName())));
error_log("Updated $className");
}
}
@maiermic
Copy link
Author

maiermic commented Nov 7, 2018

For example, the doc comment of class Foo

<?php

namespace App;

/**
 * Documentation of class Foo
 */
class Foo
{
    /** @var string */
    private $noSetOrGet;

    /** @var int */
    private $hasSet;

    /** @var string[] */
    private $hasGet;

    /** @var int */
    private $hasGetAndSet;

    /**
     * @return string[]
     */
    public function getHasGet(): array
    {
        return $this->hasGet;
    }

    /**
     * @return int
     */
    public function getHasGetAndSet(): int
    {
        return $this->hasGetAndSet;
    }

    /**
     * @param int $hasSet
     */
    public function setHasSet(int $hasSet): void
    {
        $this->hasSet = $hasSet;
    }

    /**
     * @param int $hasGetAndSet
     */
    public function setHasGetAndSet(int $hasGetAndSet): void
    {
        $this->hasGetAndSet = $hasGetAndSet;
    }

    /**
     * Magic getter and setter method.
     */
    public function __call($name, $arguments)
    {
        if (substr($name, 0, 3) == 'get') {
            return $this->{lcfirst(substr($name, 3))};
        }
        if (substr($name, 0, 3) == 'set') {
            $this->{lcfirst(substr($name, 3))} = $arguments[0];
        }
    }
}

will be updated to

/**
 * Documentation of class Foo
 *
 * @method string getNoSetOrGet()
 * @method void setNoSetOrGet(string $noSetOrGet)
 * @method int getHasSet()
 * @method void setHasGet(string $hasGet)
 */

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment