Browse Source

Add a script to recreate a single PHP file from the repository.

Daniele Alessandri 14 years ago
parent
commit
aec3fcf3e8
1 changed files with 436 additions and 0 deletions
  1. 436 0
      bin/createSingleFile.php

+ 436 - 0
bin/createSingleFile.php

@@ -0,0 +1,436 @@
+#!/usr/bin/php
+<?php
+// This script can be used to automatically glue all the .php files of Predis 
+// into a single monolithic script file that can be used without an autoloader, 
+// just like the other previous versions of the library.
+//
+// Much of its complexity is due to the fact that we cannot simply join PHP 
+// files, but namespaces and classes definitions must follow a precise order 
+// when dealing with subclassing and inheritance. 
+//
+// The current implementation is pretty naïve, but it should do for now.
+//
+
+/* -------------------------------------------------------------------------- */
+
+class PredisFile {
+    const NS_ROOT = 'Predis';
+    private $_namespaces;
+
+    public function __construct() {
+        $this->_namespaces = array();
+    }
+
+    public static function from($libraryPath) {
+        $nsroot = self::NS_ROOT;
+        $predisFile = new PredisFile();
+        $libIterator = new RecursiveDirectoryIterator("$libraryPath$nsroot");
+
+        foreach (new RecursiveIteratorIterator($libIterator) as $classFile) {
+            if (!$classFile->isFile()) {
+                continue;
+            }
+            $namespace = strtr(str_replace($libraryPath, '', $classFile->getPath()), '/', '\\');
+            $phpNamespace = $predisFile->getNamespace($namespace);
+            if ($phpNamespace === false) {
+                $phpNamespace = new PhpNamespace($namespace);
+                $predisFile->addNamespace($phpNamespace);
+            }
+            $phpClass = new PhpClass($phpNamespace, $classFile);
+        }
+        return $predisFile;
+    }
+
+    public function addNamespace(PhpNamespace $namespace) {
+        if (isset($this->_namespaces[(string)$namespace])) {
+            throw new InvalidArgumentException("Duplicated namespace");
+        }
+        $this->_namespaces[(string)$namespace] = $namespace;
+    }
+
+    public function getNamespaces() {
+        return $this->_namespaces;
+    }
+
+    public function getNamespace($namespace) {
+        if (!isset($this->_namespaces[$namespace])) {
+            return false;
+        }
+        return $this->_namespaces[$namespace];
+    }
+
+    public function getClassByFQN($classFqn) {
+        if (($nsLastPos = strrpos($classFqn, '\\')) !== false) {
+            $namespace = $this->getNamespace(substr($classFqn, 0, $nsLastPos));
+            if ($namespace === false) {
+                return null;
+            }
+            $className = substr($classFqn, $nsLastPos + 1);
+            return $namespace->getClass($className);
+        }
+        return null;
+    }
+
+    private function calculateDependencyScores(&$classes, $fqn) {
+        if (!isset($classes[$fqn])) {
+            $classes[$fqn] = 0;
+        }
+        $classes[$fqn] += 1;
+        $phpClass = $this->getClassByFQN($fqn);
+        foreach ($phpClass->getDependencies() as $fqn) {
+            $this->calculateDependencyScores($classes, $fqn);
+        }
+    }
+
+    private function getDependencyScores() {
+        $classes = array();
+        foreach ($this->getNamespaces() as $phpNamespace) {
+            foreach ($phpNamespace->getClasses() as $phpClass) {
+                $this->calculateDependencyScores($classes, $phpClass->getFQN());
+            }
+        }
+        return $classes;
+    }
+
+    private function getOrderedNamespaces($dependencyScores) {
+        $namespaces = array_fill_keys(array_unique(
+            array_map(
+                function($fqn) { return PhpNamespace::extractName($fqn); },
+                array_keys($dependencyScores)
+            )
+        ), 0);
+        foreach ($dependencyScores as $classFqn => $score) {
+            $namespaces[PhpNamespace::extractName($classFqn)] += $score;
+        }
+        arsort($namespaces);
+        return array_keys($namespaces);
+    }
+
+    private function getOrderedClasses(PhpNamespace $phpNamespace, $classes) {
+        $nsClassesFQNs = array_map(
+            function($cl) { return $cl->getFQN(); },
+            $phpNamespace->getClasses()
+        );
+        $nsOrderedClasses = array();
+        foreach ($nsClassesFQNs as $nsClassFQN) {
+            $nsOrderedClasses[$nsClassFQN] = $classes[$nsClassFQN];
+        }
+        arsort($nsOrderedClasses);
+        return array_keys($nsOrderedClasses);
+    }
+
+    public function getPhpCode() {
+        $buffer = array("<?php\n\n");
+        $classes = $this->getDependencyScores();
+        $namespaces = $this->getOrderedNamespaces($classes);
+
+        foreach ($namespaces as $namespace) {
+            $phpNamespace = $this->getNamespace($namespace);
+
+            // generate namespace directive
+            $buffer[] = $phpNamespace->getPhpCode();
+            $buffer[] = "\n";
+
+            // generate use directives
+            $useDirectives = $phpNamespace->getUseDirectives();
+            if (count($useDirectives) > 0) {
+                $buffer[] = $useDirectives->getPhpCode();
+                $buffer[] = "\n";
+            }
+
+            // generate classes bodies
+            $nsClasses = $this->getOrderedClasses($phpNamespace, $classes);
+            foreach ($nsClasses as $classFQN) {
+                $buffer[] = $this->getClassByFQN($classFQN)->getPhpCode();
+                $buffer[] = "\n\n";
+            }
+
+            $buffer[] = "/* " . str_repeat("-", 75) . " */";
+            $buffer[] = "\n\n";
+        }
+        return implode($buffer);
+    }
+
+    public function saveTo($outputFile) {
+        // TODO: add more sanity checks
+        if ($outputFile === null || $outputFile === '') {
+            throw new InvalidArgumentException('You must specify a valid output file');
+        }
+        file_put_contents($outputFile, $this->getPhpCode());
+    }
+}
+
+class PhpNamespace implements IteratorAggregate {
+    private $_namespace, $_classes;
+
+    public function __construct($namespace) {
+        $this->_namespace = $namespace;
+        $this->_classes = array();
+        $this->_useDirectives = new PhpUseDirectives($this);
+    }
+
+    public static function extractName($fqn) {
+        $nsSepLast = strrpos($fqn, '\\');
+        if ($nsSepLast === false) {
+            return $fqn;
+        }
+        $ns = substr($fqn, 0, $nsSepLast);
+        return $ns !== '' ? $ns : null;
+            
+    }
+
+    public function addClass(PhpClass $class) {
+        $this->_classes[$class->getName()] = $class;
+    }
+
+    public function getClass($className) {
+        return $this->_classes[$className];
+    }
+
+    public function getClasses() {
+        return array_values($this->_classes);
+    }
+
+    public function getIterator() {
+        return new \ArrayIterator($this->getClasses());
+    }
+
+    public function getUseDirectives() {
+        return $this->_useDirectives;
+    }
+
+    public function getPhpCode() {
+        return "namespace $this->_namespace;\n";
+    }
+
+    public function __toString() {
+        return $this->_namespace;
+    }
+}
+
+class PhpUseDirectives implements Countable, IteratorAggregate {
+    private $_use, $_aliases, $_namespace;
+
+    public function __construct(PhpNamespace $namespace) {
+        $this->_use = array();
+        $this->_aliases = array();
+        $this->_namespace = $namespace;
+    }
+
+    public function add($use, $as = null) {
+        if (!in_array($use, $this->_use)) {
+            $this->_use[] = $use;
+            $this->_aliases[$as ?: PhpClass::extractName($use)] = $use;
+        }
+    }
+
+    public function getList() {
+        return $this->_use;
+    }
+
+    public function getIterator() {
+        return new \ArrayIterator($this->getList());
+    }
+
+    public function getPhpCode() {
+        $reducer = function($str, $use) { return $str .= "use $use;\n"; };
+        return array_reduce($this->getList(), $reducer, '');
+    }
+
+    public function getNamespace() {
+        return $this->_namespace;
+    }
+
+    public function getFQN($className) {
+        if (($nsSepFirst = strpos($className, '\\')) === false) {
+            if (isset($this->_aliases[$className])) {
+                return $this->_aliases[$className];
+            }
+            return (string)$this->getNamespace() . "\\$className";
+        }
+        if ($nsSepFirst != 0) {
+            throw new InvalidArgumentException("Partially qualified names are not supported");
+        }
+        return $className;
+    }
+
+    public function count() {
+        return count($this->_use);
+    }
+}
+
+class PhpClass {
+    private $_namespace, $_file, $_body, $_implements, $_extends, $_name;
+
+    public function __construct(PhpNamespace $namespace, SplFileInfo $classFile) {
+        $this->_namespace = $namespace;
+        $this->_file = $classFile;
+        $this->_implements = array();
+        $this->_extends = array();
+        $this->extractData();
+        $namespace->addClass($this);
+    }
+
+    public static function extractName($fqn) {
+        $nsSepLast = strrpos($fqn, '\\');
+        if ($nsSepLast === false) {
+            return $fqn;
+        }
+        return substr($fqn, $nsSepLast + 1);
+    }
+
+    private function extractData() {
+        $useDirectives = $this->getNamespace()->getUseDirectives();
+
+        $useExtractor = function($m) use($useDirectives) {
+            $useDirectives->add(($namespacedPath = $m[1]));
+        };
+
+        $classBuffer = stream_get_contents(fopen($this->getFile()->getPathname(), 'r'));
+
+        $classBuffer = preg_replace('/<\?php\s?\\n\s?/', '', $classBuffer);
+        $classBuffer = preg_replace('/\s?\?>\n?/ms', '', $classBuffer);
+        $classBuffer = preg_replace('/namespace\s+.*;\s?/', '', $classBuffer);
+        $classBuffer = preg_replace_callback('/use\s+(.*)(\s+as\s+.*)?;\s?\n?/', $useExtractor, $classBuffer);
+
+        $this->_body = trim($classBuffer);
+
+        $this->extractHierarchy();
+    }
+
+    private function extractHierarchy() {
+        $implements = array();
+        $extends =  array();
+
+        $extractor = function($iterator, $callback) {
+            $className = '';
+            $iterator->seek($iterator->key() + 1);
+            while ($iterator->valid()) {
+                $token = $iterator->current();
+                if (is_string($token)) {
+                    if (preg_match('/\s?,\s?/', $token)) {
+                        $callback(trim($className));
+                        $className = '';
+                    }
+                    else if ($token == '{') {
+                        $callback(trim($className));
+                        return;
+                    }
+                }
+                switch ($token[0]) {
+                    case T_NS_SEPARATOR:
+                        $className .= '\\';
+                        break;
+                    case T_STRING:
+                        $className .= $token[1];
+                        break;
+                    case T_IMPLEMENTS:
+                    case T_EXTENDS:
+                        $callback(trim($className));
+                        $iterator->seek($iterator->key() - 1);
+                        return;
+                }
+                $iterator->next();
+            }
+        };
+
+        $tokens = token_get_all("<?php\n" . trim($this->getPhpCode()));
+
+        $iterator = new ArrayIterator($tokens);
+        while ($iterator->valid()) {
+            $token = $iterator->current();
+            if (is_string($token)) {
+                $iterator->next();
+                continue;
+            }
+
+            switch ($token[0]) {
+                case T_CLASS:
+                case T_INTERFACE:
+                    $iterator->seek($iterator->key() + 2);
+                    $tk = $iterator->current();
+                    $this->_name = $tk[1];
+                    break;
+                case T_IMPLEMENTS:
+                    $extractor($iterator, function($fqn) use (&$implements) {
+                        $implements[] = $fqn;
+                    });
+                    break;
+                case T_EXTENDS:
+                    $extractor($iterator, function($fqn) use (&$extends) {
+                        $extends[] = $fqn;
+                    });
+                    break;
+            }
+            $iterator->next();
+        }
+
+        $this->_implements = $this->guessFQN($implements);
+        $this->_extends = $this->guessFQN($extends);
+    }
+
+    public function guessFQN($classes) {
+        $useDirectives = $this->getNamespace()->getUseDirectives();
+        return array_map(array($useDirectives, 'getFQN'), $classes);        
+    }
+
+    public function getImplementedInterfaces($all = false) {
+        if ($all) {
+            return $this->_implements;
+        }
+        else {
+            return array_filter(
+                $this->_implements,
+                function ($cn) { return strpos($cn, 'Predis\\') === 0; }
+            );
+        }
+    }
+
+    public function getExtendedClasses($all = false) {
+        if ($all) {
+            return $this->_extemds;
+        }
+        else {
+            return array_filter(
+                $this->_extends,
+                function ($cn) { return strpos($cn, 'Predis\\') === 0; }
+            );
+        }
+    }
+
+    public function getDependencies($all = false) {
+        return array_merge(
+            $this->getImplementedInterfaces($all),
+            $this->getExtendedClasses($all)
+        );
+    }
+
+    public function getNamespace() {
+        return $this->_namespace;
+    }
+
+    public function getFile() {
+        return $this->_file;
+    }
+
+    public function getName() {
+        return $this->_name;
+    }
+
+    public function getFQN() {
+        return (string)$this->getNamespace() . '\\' . $this->_name;
+    }
+
+    public function getPhpCode() {
+        return $this->_body;
+    }
+
+    public function __toString() {
+        return "class " . $this->getName() . '{ ... }';
+    }
+}
+
+/* -------------------------------------------------------------------------- */
+
+$predisFile = PredisFile::from(__DIR__ . "/../lib/");
+$predisFile->saveTo(isset($argv[1]) ? $argv[1] : PredisFile::NS_ROOT . ".php");