<?php
/**
* api: php
* title: Phar generation
* description: PHP script template to get Phar assembled
* version: 0.3
*
* Used as template php script for -t phar building.
* Extracts passed Ruby variables as PHP strings, so some
* need constant(), strlen() or boolean interpretation.
*
*/
#-- Parameters from phar.rb passed as JSON in argv[1]
extract(json_decode($_SERVER["argv"][1], TRUE));
/**
* Create phar
* · Tempoarary filename.
* · FPM package name as Phar name.
* · Read complete staging dir.
*/
$p = new Phar($tmpf, 0, $name);
$p->startBuffering();
$p->buildFromDirectory($srcdir);
/**
* Add stub
* · Use given file
* · Default init+index.php for native Phars.
* · Stub for zip/tar archive.
*/
if (strlen($stub) && file_exists($stub)) {
$p->setStub(file_get_contents($stub));
}
elseif (constant($format) == Phar::PHAR) {
$p->setDefaultStub("__init__.php", "index.php");
}
else {
$p->setDefaultStub();
}
/**
* Phar meta data
* · Use injected JSON blob.
* · Contains at least fpm package infos,
* · May contain additional fields - attrs{} from `-s src` or `composer` module
* · Also build and include a class `map` structure.
*/
if ($meta) {
$meta["map"] = map_phar($p, $srcdir);
$p->setMetadata($meta);
}
/**
* Complete packaging
* · Set per-file compression
* · Add signature, if any.
* · Compressed outer archive hull (tar or phar).
*/
if (constant($filegz)) {
$p->compressFiles(constant($filegz));
}
if (strlen($sign)) {
$p->setSignatureAlgorithm(Phar::OPENSSL, file_get_contents($sign));
}
else {
$p->setSignatureAlgorithm(Phar::SHA256);
}
// Whole-archive compression; output goes to a different filename. (Cleaned up in Ruby...)
$p->stopBuffering();
if (strlen($extout)) {
$p->compress(constant($hullgz));
}
/**
* Traverse Phar entries and augment Phar meta class/function/const `map`,
* which lists identifiers as "vnd\pkg\class" => "internal-filename.php".
*
* Needs to manually reread the directory and files, because freshly-built
* Phars` RecursiveDirectoryIterator + offsetGet() return zilch.
*
*/
function map_phar($p, $dir) {
// prepare empty map
$map = array("class"=>array(), "function"=>array(), "const"=>array());
// switch to staging dir, but keep old path as reference
$it = new RecursiveIteratorIterator(new RecursiveDirectoryIterator($dir));
$it = new RegexIterator($it, "~^(?!.*tests?/).+\.php$~");
// only read .php files
foreach ($it as $fn) {
// cut off $dir prefix
$int_fn = ltrim(substr($fn, strlen($dir)), "/");
// collect identifiers
$def = new RegexPhpIdentifierDeclarations(file_get_contents($fn));
foreach ($def->identifiers() as $type=>$list) {
$map[$type] = array_merge($map[$type], array_fill_keys($list, $int_fn));
}
}
// prepare for PHP compliant case-insensitive lookups
$map["class"] = array_change_key_case($map["class"]);
$map["function"] = array_change_key_case($map["function"]);
return $map;
}
/**
* @source shared.phar
* @license Public Domain
*
* Shallow regex-lexing to uncover namespace/class/function identifiers.
*
* By relying on keyword context and a bit of block-level skipping, this still
* uncovers correctly nested and deferred declaration constructs. Plain function
* injections within methods however are overlooked. Dynamic declarations within
* strings are ignored due to non-code being stripped beforehand.
*
* This approach doesn't assert any nesting/syntax correctness; as implementing
* it per recursive subroutines wouldn't provide anything like a parse tree via
* PCREs interface / and else inverted the speed advantage here.
*
*/
class RegexPhpIdentifierDeclarations {
/**
* Regex all the things.
*
*/
public function __construct($source) {
/**
* Remove non-code sections (comments and strings actually),
* but convert define() string into constant literal before.
*
*/
$source = preg_replace(
"~\b define \s*\(\s* ([\"\']) ([\\w\\x7F-\\xFF]+) \\1 \s*, ~ix",
"const $2 =", $source
);
$source = preg_replace("~
(?: \A | \?\>) .*? \<\?(?:php|=)+? # Open+closing PHP token
| /\* .*? \*/ # Multiline /* comments */
| // \V* # Singe line // comment
| \# \V* # Hash comment
| \" (?:[^\"\\\\] | \\\\.)* \" # Double quoted string
| \' (?:[^\'\\\\] | \\\\.)* \' # Single quoted string
| <<<\s* (\w+) .+? ^\\1 # Heredoc string
| <<<\s* '(\V+)' .+? ^\\1 # Nowdoc string
~smix",
"", $source
);
/**
* Match identifiers and skip class block {} structures. (While one could recurse
* into methods or namespace{} blocks individually, practically only the outermost
* interface is relevant for the autoloader.)
*
*/
preg_match_all("~
(?: (?<![\\x7F-\\xFF]) \b ) # Only match constructs at word breaks
(?:
namespace \s+
([\\w\\x7F-\\xFF\\\\]+) \s* [{;] # Namespace identifier
| (?is:class|interface|trait) \s+
([\\w\\x7F-\\xFF]+) [^\{\}]* # Class declaration
((?>\{ (?: [^\{\}]* | (?-1) )*\})) # Recursive {...} block skipping
| function \s+
([\\w\\x7F-\\xFF]+) \s* \( # Plain functions
| (?is: const\s+| define\s*\( )
([\\w\\x7F-\\xFF]+) \s* [=,] # Constants (const/define)
)~ix",
$source, $this->matches, PREG_SET_ORDER
);
}
/**
* Nested array of identifier strings
* → Namespaces in [1]
* → Classes in [2]
* → Function names in [4]
* → Constants in [5]
*
*/
var $matches = array();
/**
* Join matched namespace and construct strings into our beloved named identifier groups.
*
*/
public function identifiers() {
// Result list, and current $ns namespace
$r = array(
"class" => array(),
"function" => array(),
"const" => array(),
);
$ns = "";
/**
* Check match group for entries.
* Probe in order of likelihood, at least one will be there. And since identifiers
* with leading zeros are invalid, the plain truthy test is preferrable to strlen.
*
*/
foreach ($this->matches as $name) {
if ($name[1]) {
$ns = $name[1] . "\\";
}
elseif ($name[2]) {
$r["class"][] = $ns . $name[2];
}
elseif ($name[4]) {
$r["function"][] = $ns . $name[4];
}
elseif ($name[5]) {
$r["const"][] = $ns . $name[5];
}
}
return $r;
}
}