Skip to content

Instantly share code, notes, and snippets.

@Potherca
Last active October 20, 2023 20:40
Show Gist options
  • Save Potherca/594adece80ad1e9ddbce0b207db8ed0c to your computer and use it in GitHub Desktop.
Save Potherca/594adece80ad1e9ddbce0b207db8ed0c to your computer and use it in GitHub Desktop.
PHP Split file with multiple classes into separate files.

Split a file with mulitple classes into separate files.

Installation

git clone git@gist.github.com:594adece80ad1e9ddbce0b207db8ed0c.git split-file
cd split-file
composer install

Usage:

split-file [options] <subject-file> <target-directory> [path-code]

With options:

--help      Display this help message
--dry-run   Show what would be written

Where:

- <subject-file>     is the file that is to be split.
- <target-directory> is the directory that split class files are to be written to. Must already exist.
- path-code          is the PHP code that is used to replace a found class with. Please be aware this MUST be valid PHP.
                     In order to base the file-name on the name of each split of class, use '%class%'. 

Example:

php /path/to/split-file.php --dry-run '/path/to/multi.class.php' '/path/to/classes/' "__DIR__.'/%class%.php'"

Please note that support for preserving code formatting depends on the parser used by this project. Support is still experimental so until it is stable, the split-out code might contain formatting changes.

{
"name": "potherca/php-file-splitter",
"description": "Split a file with multiple classes into separate files.",
"type": "project",
"license": "GPL-3.0-or-later",
"authors": [
{
"name": "Ben Peachey",
"email": "potherca@gmail.com"
}
],
"require": {
"nikic/php-parser": "^4.0"
},
"autoload": {
"psr-4": {
"Potherca\\FileSplitter\\": "./"
}
},
"config": {
"sort-packages": true
}
}
<?php
namespace Potherca\FileSplitter;
use PhpParser\NodeTraverser;
use PhpParser\NodeVisitor\NameResolver;
set_error_handler(function ($severity, $message, $file, $line) {
throw new \ErrorException($message, 0, $severity, $file, $line);
});
$parameters = $argv;
$command = basename(array_shift($parameters));
if (count($parameters) < 2 || in_array('--help', $parameters)) {
echo <<<TXT
Usage:
{$command} [options] <subject-file> <target-directory> [path-code]
With options:
--help Display this help message
--dry-run Show what would be written
Where:
- <subject-file> is the file that is to be split.
- <target-directory> is the directory that split class files are to be written to. Must already exist.
- path-code is the PHP code that is used to replace a found class with. Please be aware this MUST be valid PHP.
In order to base the file-name on the name of each split of class, use '%class%'.
Example:
{$command} --dry-run 'path/to/multi.class.php' 'path/to/classes/' "__DIR__.'/%class%.php'"
Please note that support for preserving code formatting depends on the parser used by this project.
Support is still experimental so until it is stable, the split-out code might contain formatting changes.
TXT;
} else {
require __DIR__.'/vendor/autoload.php';
/*/ Create variables from the command-line parameters /*/
$dryRun = false;
$parameters = array_map(function ($parameter) use (&$dryRun) {
$isNamedParam = strpos($parameter, '--') === 0;
if ($isNamedParam && $parameter === '--dry-run') {
$dryRun = true;
}
return $isNamedParam?null:$parameter;
}, $parameters);
$filePath = array_shift($parameters);
$targetPath = rtrim(array_shift($parameters), '/');
$pathCode = array_shift($parameters);
// @FIXME: Warn if {$pathCode} is not valid (i.e. can not be parsed).
if (is_file($filePath) === false) {
throw new \InvalidArgumentException("Could not find file at given path {$filePath}");
} elseif (is_dir($targetPath) === false) {
throw new \InvalidArgumentException("Could not find directory at given path {$targetPath}");
} else {
parseCode($filePath, $pathCode, $targetPath, $dryRun);
}
}
/*EOF*/
/**
* @param string $filePath
* @param string $pathCode
* @param string $targetPath
* @param bool $dryRun
*/
function parseCode($filePath, $pathCode, $targetPath, $dryRun)
{
$code = file_get_contents($filePath);
/*/ Set up Parser /*/
$lexer = new \PhpParser\Lexer\Emulative([
'usedAttributes' => [
'comments',
'startLine',
'endLine',
'startTokenPos',
'endTokenPos',
],
]);
$parser = new \PhpParser\Parser\Php5($lexer);
$traverser = new NodeTraverser();
$traverser->addVisitor(new \PhpParser\NodeVisitor\CloningVisitor());
$splitClassVisitor = new SplitClassVisitor($pathCode);
$traverser->addVisitor($splitClassVisitor);
$traverser->addVisitor(new NameResolver(null, [
'preserveOriginalNames' => false,
'replaceNodes' => true,
]));
/*/ Parse the given file /*/
$originalStaments = $parser->parse($code);
$originalTokens = $lexer->getTokens();
$statements = $traverser->traverse($originalStaments);
if (is_array($statements)) {
/*/ Convert AST to string /*/
$printer = new \PhpParser\PrettyPrinter\Standard();
$files = [];
// Add the code of each class
$classNodes = $splitClassVisitor->getClassNodes();
array_walk($classNodes,
function ($node, $name) use (&$files, $printer, $targetPath, $originalStaments, $originalTokens) {
$path = "{$targetPath}/{$name}.php";
//$code = $printer->prettyPrintFile([$node])."\n";
$code = $printer->printFormatPreserving([$node], $originalStaments, $originalTokens) . "\n";
$files[$path] = $code;
});
// Add the original file
//$files[$file] = $printer->prettyPrintFile($statements)."\n";
$files[$filePath] = $printer->printFormatPreserving($statements, $originalStaments, $originalTokens) . "\n";
/*/ Actually write the files /*/
array_walk($files, function ($content, $path) use ($dryRun) {
if ($dryRun === true) {
echo "\n# ================================================================================\nFILE: $path\nCONTENT: $content";
} else {
echo "Writing content to {$path}\n";
file_put_contents($path, $content);
}
});
}
}
<?php
namespace Potherca\FileSplitter;
use PhpParser\Error;
use PhpParser\Node;
use PhpParser\NodeVisitorAbstract;
use PhpParser\Parser;
use PhpParser\ParserFactory;
class SplitClassVisitor extends NodeVisitorAbstract
{
/** @var string */
private $pathCode;
/** @var Node[] */
private $classNodes = [];
/** @return Node[] */
final public function getClassNodes()
{
return $this->classNodes;
}
/** @return Parser */
private function getParser()
{
static $parser;
if ($parser === null) {
$parser = (new ParserFactory())->create(ParserFactory::PREFER_PHP5);
}
return $parser;
}
final public function __construct($pathCode)
{
$this->pathCode = (string) $pathCode;
}
final public function leaveNode(Node $node)
{
$statement = null;
if ($node instanceof Node\Stmt\Class_) {
$name = (string) $node->name;
$this->classNodes[$name] = $node;
$statement = $this->createRequireStatement($name);
}
return $statement;
}
private function createRequireStatement($name)
{
$statements = '';
$code = '';
if ($this->pathCode !== '') {
$path = strtr($this->pathCode, ['%class%' => $name]);
$code = "<?php require_once {$path}; ?>";
}
try {
$statements = $this->getParser()->parse($code);
} catch (Error $exception) {
throw new \InvalidArgumentException('Could not parse given path code "'.$this->pathCode.'". Please check your syntax is valid.');
}
return $statements;
}
}
/*EOF*/
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment