Skip to content

Instantly share code, notes, and snippets.

@oplanre
Last active January 28, 2025 15:46
Show Gist options
  • Save oplanre/a8320becd9dce8c690dafbf8fedc46c7 to your computer and use it in GitHub Desktop.
Save oplanre/a8320becd9dce8c690dafbf8fedc46c7 to your computer and use it in GitHub Desktop.
PHP script to generate a single IDE stub file from php files in a project
<?php
require_once __DIR__ . "/bootstrap.php";
use PhpParser\Error;
use PhpParser\Node\Stmt\ClassMethod;
use PhpParser\Node\Stmt\Function_;
use PhpParser\Node\Stmt\Interface_;
use PhpParser\NodeTraverser;
use PhpParser\ParserFactory;
use PhpParser\PrettyPrinter;
use PhpParser\Node;
use PhpParser\NodeVisitorAbstract;
define('PARSER', (new ParserFactory)->createForHostVersion());
define('PRINTER', new class extends PrettyPrinter\Standard {
private bool $inInterface = false;
private function printFunctionLike(
array $attrGroups,
Node\Identifier $name,
array $params,
?array $stmts,
string $functionType = '',
Node|null $returnType = null,
bool $abstract = false,
bool $byRef = false,
): string {
return $this->pAttrGroups($attrGroups)
. $functionType . 'function ' . ($byRef ? '&' : '') . $name
. '(' . $this->pMaybeMultiline($params, $this->phpVersion->supportsTrailingCommaInParamList()) . ')'
. (null !== $returnType ? ': ' . $this->p($returnType) : '')
. (
($this->inInterface || $abstract || null === $stmts)
? ';'
: (count($stmts) === 0 ? ' {}' : ' {' . $this->nl . $this->pStmts($stmts) . $this->nl . '}'
)
);
}
protected function pStmt_ClassMethod(ClassMethod $node): string
{
return $this->printFunctionLike(
attrGroups: $node->attrGroups,
functionType: $this->pModifiers($node->flags),
name: $node->name,
params: $node->params,
stmts: $node->stmts,
returnType: $node->returnType,
abstract: $node->isAbstract(),
byRef: $node->byRef,
);
}
protected function pStmt_Function(Function_ $node): string
{
return $this->printFunctionLike(
attrGroups: $node->attrGroups,
name: $node->name,
params: $node->params,
stmts: $node->stmts,
returnType: $node->returnType,
);
}
protected function pStmt_Interface(Interface_ $node): string
{
$this->inInterface = true;
$out = $this->pAttrGroups($node->attrGroups)
. 'interface ' . $node->name
. (!empty($node->extends) ? ' extends ' . $this->pCommaSeparated($node->extends) : '')
. $this->nl . '{' . $this->pStmts($node->stmts) . $this->nl . '}';
$this->inInterface = false;
return $out;
}
});
define('VISITOR', new class extends NodeVisitorAbstract {
public function leaveNode(Node $node)
{
if ($node instanceof Node\Stmt\Function_ || ($node instanceof Node\Stmt\ClassMethod && !$node->isPrivate())) {
$node->stmts = [];
return $node;
}
if (
(
$node instanceof Node\Stmt\Use_ ||
$node instanceof Node\Stmt\GroupUse ||
$node instanceof Node\Stmt\Declare_
) ||
((
$node instanceof Node\Stmt\ClassConst ||
$node instanceof Node\Stmt\ClassMethod ||
$node instanceof Node\Stmt\Property
) && $node->isPrivate())
) {
return NodeTraverser::REMOVE_NODE;
}
if ($node instanceof Node\Stmt\PropertyProperty) {
$node->default = null;
return $node;
}
if ($node instanceof Node\Stmt\ClassLike) {
return new Node\Stmt\If_(
cond: new Node\Expr\BooleanNot(
new Node\Expr\FuncCall(
new Node\Name(match (true) {
$node instanceof Node\Stmt\Enum_ => 'enum_exists',
$node instanceof Node\Stmt\Interface_ => 'interface_exists',
$node instanceof Node\Stmt\Trait_ => 'trait_exists',
default => 'class_exists',
}),
[
new Node\Arg(
new Node\Scalar\String_(
$node->namespacedName?->toString() ?? $node->name?->name ?? ''
),
),
],
),
),
subNodes: ['stmts' => [$node]],
);
}
if (!$node instanceof Node\Stmt\Namespace_) {
$node->setAttribute('comments', []);
}
return $node;
}
});
class Stubbifier
{
private array $namespaces = [];
public function __construct() {}
public function walkNodes(array $nodes)
{
foreach ($nodes as $node) {
if ($node instanceof Node\Stmt\Namespace_) {
// If we find a namespace, process its statements recursively
$this->processNamespace($node);
} else {
// Handle non-namespace nodes (could go into "global" namespace)
$this->ns(null, [$node]);
}
}
}
private function processNamespace(Node\Stmt\Namespace_ $namespace)
{
$stmts = [];
$nestedNamespaces = [];
// Separate nested namespaces from other statements
foreach ($namespace->stmts as $stmt) {
if ($stmt instanceof Node\Stmt\Namespace_) {
$nestedNamespaces[] = $stmt;
} else {
$stmts[] = $stmt;
}
}
// Handle the current namespace's direct statements
if ($stmts) {
$this->ns($namespace->name, $stmts, $namespace->getAttributes());
}
// Process any nested namespaces
foreach ($nestedNamespaces as $nested) {
if ($namespace->name !== null) {
// Combine the namespace names
$newName = Node\Name::concat($namespace->name, $nested->name);
$nested->name = $newName;
}
$this->processNamespace($nested);
}
}
public function ns(?Node\Name $name, array $stmts, array $attributes = [])
{
$key = $name?->toString() ?? '';
$ns = $this->namespaces[$key] ?? new Node\Stmt\Namespace_(
name: $name,
attributes: array_merge($attributes, [
'kind' => Node\Stmt\Namespace_::KIND_BRACED,
]),
);
$ns->stmts = array_merge($ns->stmts ?? [], $stmts);
$this->namespaces[$key] = $ns;
}
public function __invoke(\Traversable $it, string $dest = STUB_DIR . "/ptst.stub.php")
{
try {
foreach ($it as $file) {
$sourceCode = file_get_contents($file->getRealPath());
$traverser = new NodeTraverser;
$traverser->addVisitor(new PhpParser\NodeVisitor\NameResolver);
$traverser->addVisitor(VISITOR);
$this->walkNodes(
$traverser->traverse(
PARSER->parse($sourceCode),
),
);
}
@mkdir(dirname($dest), 0777, true);
file_put_contents(
$dest,
PRINTER->prettyPrintFile(array_values($this->namespaces)),
);
} catch (Error $error) {
throw new RuntimeException("Parse error: {$error->getMessage()}");
}
}
}
exec("rm -rf " . STUB_DIR);
(new Stubbifier)((new Symfony\Component\Finder\Finder)->files()->in(PHP_DIR . "/src")->name("*.php"));
echo "Stubs generated\n";
@ISTIFANO
Copy link

good

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