mirror of
https://github.com/okdana/twigc.git
synced 2024-05-08 08:26:34 +02:00
Add source
This commit is contained in:
parent
296717820c
commit
48fb6be6e6
24
bin/compile
Normal file
24
bin/compile
Normal file
|
@ -0,0 +1,24 @@
|
|||
#!/usr/bin/env php
|
||||
<?php
|
||||
|
||||
/**
|
||||
* This file is part of twigc.
|
||||
*
|
||||
* @author dana geier <dana@dana.is>
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
require_once __DIR__ . '/../src/bootstrap.php';
|
||||
|
||||
$verbose = false;
|
||||
$verboseArgs = ['v', 'vv', 'vvv', 'verbose', 'debug'];
|
||||
|
||||
foreach ( $argv as $arg ) {
|
||||
if ( in_array(ltrim($arg, '-'), $verboseArgs, true) ) {
|
||||
$verbose = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
(new \Dana\Twigc\PharCompiler($verbose))->compile();
|
||||
|
14
bin/twigc
Normal file
14
bin/twigc
Normal file
|
@ -0,0 +1,14 @@
|
|||
#!/usr/bin/env php
|
||||
<?php
|
||||
|
||||
/**
|
||||
* This file is part of twigc.
|
||||
*
|
||||
* @author dana geier <dana@dana.is>
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
require_once __DIR__ . '/../src/bootstrap.php';
|
||||
|
||||
(new \Dana\Twigc\Application())->run();
|
||||
|
340
src/DefaultCommand.php
Normal file
340
src/DefaultCommand.php
Normal file
|
@ -0,0 +1,340 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* This file is part of twigc.
|
||||
*
|
||||
* @author dana geier <dana@dana.is>
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
namespace Dana\Twigc;
|
||||
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\Helper\DescriptorHelper;
|
||||
use Symfony\Component\Console\Helper\Table;
|
||||
use Symfony\Component\Console\Input\InputArgument;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Input\InputOption;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
|
||||
/**
|
||||
* Default twigc command.
|
||||
*/
|
||||
class DefaultCommand extends Command {
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function configure() {
|
||||
$this
|
||||
->setName('twigc')
|
||||
->setDescription('Compile a Twig template')
|
||||
->addArgument(
|
||||
'template',
|
||||
InputArgument::OPTIONAL,
|
||||
'A Twig template file to process (use `-` for STDIN)'
|
||||
)
|
||||
->addOption(
|
||||
'help',
|
||||
'h',
|
||||
InputOption::VALUE_NONE,
|
||||
'Display this usage help'
|
||||
)
|
||||
->addOption(
|
||||
'version',
|
||||
'V',
|
||||
InputOption::VALUE_NONE,
|
||||
'Display version information'
|
||||
)
|
||||
->addOption(
|
||||
'credits',
|
||||
null,
|
||||
InputOption::VALUE_NONE,
|
||||
'Display dependency credits (including Twig version)'
|
||||
)
|
||||
->addOption(
|
||||
'dir',
|
||||
'd',
|
||||
InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY,
|
||||
'Add search directory to loader'
|
||||
)
|
||||
->addOption(
|
||||
'escape',
|
||||
'e',
|
||||
InputOption::VALUE_REQUIRED,
|
||||
'Set autoescape environment option'
|
||||
)
|
||||
->addOption(
|
||||
'json',
|
||||
'j',
|
||||
InputOption::VALUE_REQUIRED,
|
||||
'Pass variables as JSON (dictionary string or file path)'
|
||||
)
|
||||
->addOption(
|
||||
'pair',
|
||||
'p',
|
||||
InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY,
|
||||
'Pass variable as key=value pair'
|
||||
)
|
||||
->addOption(
|
||||
'strict',
|
||||
's',
|
||||
InputOption::VALUE_NONE,
|
||||
'Enable strict_variables environment option'
|
||||
)
|
||||
;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function execute(InputInterface $input, OutputInterface $output) {
|
||||
switch ( true ) {
|
||||
// Display usage help
|
||||
case $input->getOption('help'):
|
||||
return $this->doHelp($input, $output);
|
||||
// Display version information
|
||||
case $input->getOption('version'):
|
||||
return $this->doVersion($input, $output);
|
||||
// Display package credits
|
||||
case $input->getOption('credits'):
|
||||
return $this->doCredits($input, $output);
|
||||
}
|
||||
// Render Twig template
|
||||
return $this->doRender($input, $output);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*
|
||||
* Overriding this prevents TextDescriptor from displaying the Help section.
|
||||
*/
|
||||
public function getProcessedHelp() {
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Displays usage help.
|
||||
*
|
||||
* @param InputInterface $input
|
||||
* @param OutputInterface $output
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function doHelp(InputInterface $input, OutputInterface $output) {
|
||||
(new DescriptorHelper())->describe($output, $this);
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Displays version information.
|
||||
*
|
||||
* @param InputInterface $input
|
||||
* @param OutputInterface $output
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function doVersion(InputInterface $input, OutputInterface $output) {
|
||||
$output->writeln(sprintf(
|
||||
'twigc version %s (%s @ %s)',
|
||||
\Dana\Twigc\Twigc::VERSION_NUMBER,
|
||||
\Dana\Twigc\Twigc::VERSION_COMMIT,
|
||||
\Dana\Twigc\Twigc::VERSION_DATE
|
||||
));
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Displays package credits.
|
||||
*
|
||||
* @param InputInterface $input
|
||||
* @param OutputInterface $output
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function doCredits(InputInterface $input, OutputInterface $output) {
|
||||
$installed = \Dana\Twigc\Twigc::getComposerPackages();
|
||||
|
||||
$table = new Table($output);
|
||||
$table->setStyle('compact');
|
||||
$table->getStyle()->setVerticalBorderChar('');
|
||||
$table->getStyle()->setCellRowContentFormat('%s ');
|
||||
$table->setHeaders(['name', 'version', 'licence']);
|
||||
|
||||
foreach ( $installed as $package ) {
|
||||
$table->addRow([
|
||||
$package->name,
|
||||
ltrim($package->version, 'v'),
|
||||
implode(', ', $package->license) ?: '?',
|
||||
]);
|
||||
}
|
||||
$table->render();
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders a Twig template.
|
||||
*
|
||||
* @param InputInterface $input
|
||||
* @param OutputInterface $output
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function doRender(InputInterface $input, OutputInterface $output) {
|
||||
$inputData = [];
|
||||
$template = $input->getArgument('template');
|
||||
$template = $template === null ? '-' : $template;
|
||||
$dirs = $template === '-' ? [] : [dirname($template)];
|
||||
$dirs = array_merge($dirs, $input->getOption('dir'));
|
||||
$temp = false;
|
||||
$strict = (bool) $input->getOption('strict');
|
||||
$escape = $input->getOption('escape');
|
||||
|
||||
// If we're reading from STDIN, but STDIN is a TTY, print help and die
|
||||
if ( $template === '-' && posix_isatty(\STDIN) ) {
|
||||
$this->doHelp($input, $output);
|
||||
return 1;
|
||||
}
|
||||
|
||||
// Validate search directories
|
||||
foreach ( $dirs as $dir ) {
|
||||
if ( ! is_dir($dir) ) {
|
||||
throw new \InvalidArgumentException(
|
||||
"Illegal search directory: ${dir}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Normalise auto-escape setting
|
||||
if ( $escape === null ) {
|
||||
$escape = true;
|
||||
} else {
|
||||
$bool = filter_var($escape, \FILTER_VALIDATE_BOOLEAN, \FILTER_NULL_ON_FAILURE);
|
||||
|
||||
if ( $bool !== null ) {
|
||||
$escape = $bool;
|
||||
} else {
|
||||
$escape = strtolower($escape);
|
||||
}
|
||||
}
|
||||
|
||||
// Because Console doesn't allow us to see the order of options supplied
|
||||
// at the command line, there's no good way to handle precedence of
|
||||
// JSON data vs key=value pairs... so let's just disallow using them
|
||||
// together at all
|
||||
if ( $input->getOption('json') !== null && ! empty($input->getOption('pair')) ) {
|
||||
throw new \InvalidArgumentException(
|
||||
'-j and -p options may not be used together'
|
||||
);
|
||||
}
|
||||
|
||||
// Input data supplied via JSON
|
||||
if ( ($json = $input->getOption('json')) !== null ) {
|
||||
$json = trim($json);
|
||||
|
||||
// JSON supplied via STDIN
|
||||
if ( $json === '-' ) {
|
||||
if ( $template === '-' ) {
|
||||
throw new \InvalidArgumentException(
|
||||
'Can not read both template and JSON input from STDIN'
|
||||
);
|
||||
}
|
||||
$json = file_get_contents('php://stdin');
|
||||
// JSON supplied via file
|
||||
} elseif ( $json && $json[0] !== '{' ) {
|
||||
if ( ! is_file($json) ) {
|
||||
throw new \InvalidArgumentException(
|
||||
"Missing or illegal JSON file name: ${json}"
|
||||
);
|
||||
}
|
||||
$json = file_get_contents($json);
|
||||
}
|
||||
|
||||
// This check is here to prevent errors if the input is just empty
|
||||
if ( trim($json) !== '' ) {
|
||||
$inputData = json_decode($json, true);
|
||||
}
|
||||
|
||||
if ( ! is_array($inputData) ) {
|
||||
throw new \InvalidArgumentException(
|
||||
'JSON input must be a dictionary'
|
||||
);
|
||||
}
|
||||
|
||||
// Input data supplied via key=value pair
|
||||
} elseif ( count($input->getOption('pair')) ) {
|
||||
foreach ( $input->getOption('pair') as $pair ) {
|
||||
$kv = explode('=', $pair, 2);
|
||||
|
||||
if ( count($kv) !== 2 ) {
|
||||
throw new \InvalidArgumentException(
|
||||
"Illegal key=value pair: ${pair}"
|
||||
);
|
||||
}
|
||||
|
||||
$inputData[$kv[0]] = $kv[1];
|
||||
}
|
||||
}
|
||||
|
||||
// Validate key names now
|
||||
foreach ( $inputData as $key => $value ) {
|
||||
if ( ! preg_match('#^[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*$#', $key) ) {
|
||||
throw new \InvalidArgumentException(
|
||||
"Illegal variable name: ${key}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Template supplied via STDIN
|
||||
if ( $template === '-' ) {
|
||||
// If we've been supplied one or more search directories, we'll need
|
||||
// to write the template out to a temp directory so we can use the
|
||||
// file-system loader
|
||||
if ( $dirs ) {
|
||||
$temp = true;
|
||||
$template = implode('/', [
|
||||
sys_get_temp_dir(),
|
||||
implode('.', ['twigc', getmypid(), md5(time())]),
|
||||
'-',
|
||||
]);
|
||||
|
||||
mkdir(dirname($template));
|
||||
file_put_contents($template, file_get_contents('php://stdin'), LOCK_EX);
|
||||
|
||||
$dirs = array_merge([dirname($template)], $dirs);
|
||||
|
||||
$loader = new \Twig_Loader_Filesystem($dirs);
|
||||
|
||||
// Otherwise, we can just use the array loader, which is a little
|
||||
// faster and cleaner
|
||||
} else {
|
||||
$loader = new \Twig_Loader_Array([
|
||||
$template => file_get_contents('php://stdin'),
|
||||
]);
|
||||
}
|
||||
|
||||
// Template supplied via file path
|
||||
} else {
|
||||
$loader = new \Twig_Loader_Filesystem($dirs);
|
||||
}
|
||||
|
||||
try {
|
||||
$twig = new \Twig_Environment($loader, [
|
||||
'cache' => false,
|
||||
'debug' => false,
|
||||
'strict_variables' => $strict,
|
||||
'autoescape' => $escape,
|
||||
]);
|
||||
|
||||
$output->writeln(
|
||||
rtrim($twig->render(basename($template), $inputData), "\r\n")
|
||||
);
|
||||
} finally {
|
||||
if ( $temp ) {
|
||||
unlink($template);
|
||||
rmdir(dirname($template));
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
99
src/Twigc/Application.php
Normal file
99
src/Twigc/Application.php
Normal file
|
@ -0,0 +1,99 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* This file is part of twigc.
|
||||
*
|
||||
* @author dana geier <dana@dana.is>
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
namespace Dana\Twigc;
|
||||
|
||||
use Symfony\Component\Console\Application as BaseApplication;
|
||||
use Symfony\Component\Console\Input\ArrayInput;
|
||||
use Symfony\Component\Console\Input\InputArgument;
|
||||
use Symfony\Component\Console\Input\InputDefinition;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
|
||||
/**
|
||||
* twigc application container.
|
||||
*
|
||||
* This class overrides a bunch of the default Console behaviour to make the
|
||||
* application work like a more traditional UNIX CLI tool.
|
||||
*/
|
||||
class Application extends BaseApplication {
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function __construct($name = 'UNKNOWN', $version = 'UNKNOWN') {
|
||||
parent::__construct('twigc', \Dana\Twigc\Twigc::VERSION_NUMBER);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*
|
||||
* In a normal Console application, this method handles the --version and
|
||||
* --help options. In our application, the default command handles all of
|
||||
* that.
|
||||
*/
|
||||
public function doRun(InputInterface $input, OutputInterface $output) {
|
||||
$name = $this->getCommandName($input);
|
||||
|
||||
if ( ! $name ) {
|
||||
$name = $this->defaultCommand;
|
||||
$input = new ArrayInput(['command' => $this->defaultCommand]);
|
||||
}
|
||||
|
||||
$command = $this->find($name);
|
||||
|
||||
$this->runningCommand = $command;
|
||||
$exitCode = $this->doRunCommand($command, $input, $output);
|
||||
$this->runningCommand = null;
|
||||
|
||||
return $exitCode;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getDefinition() {
|
||||
$definition = parent::getDefinition();
|
||||
$definition->setArguments();
|
||||
return $definition;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*
|
||||
* Since we're a one-command application, we always use the name of the
|
||||
* default command.
|
||||
*/
|
||||
protected function getCommandName(InputInterface $input) {
|
||||
return $this->getDefaultCommands()[0]->getName();
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*
|
||||
* Since we're a one-command application, we always use the definition of
|
||||
* the default command. This means that none of the built-in Console options
|
||||
* like --help and --ansi are automatically defined — the default command
|
||||
* must handle all of that.
|
||||
*/
|
||||
protected function getDefaultInputDefinition() {
|
||||
return $this->getDefaultCommands()[0]->getDefinition();
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*
|
||||
* Since we're a one-command application, we always return just the default
|
||||
* command.
|
||||
*/
|
||||
protected function getDefaultCommands() {
|
||||
return [new \Dana\Twigc\DefaultCommand()];
|
||||
}
|
||||
|
||||
}
|
||||
|
368
src/Twigc/DefaultCommand.php
Normal file
368
src/Twigc/DefaultCommand.php
Normal file
|
@ -0,0 +1,368 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* This file is part of twigc.
|
||||
*
|
||||
* @author dana geier <dana@dana.is>
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
namespace Dana\Twigc;
|
||||
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\Helper\DescriptorHelper;
|
||||
use Symfony\Component\Console\Helper\Table;
|
||||
use Symfony\Component\Console\Input\InputArgument;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Input\InputOption;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
|
||||
/**
|
||||
* Default twigc command.
|
||||
*/
|
||||
class DefaultCommand extends Command {
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function configure() {
|
||||
$this
|
||||
->setName('twigc')
|
||||
->setDescription('Compile a Twig template')
|
||||
->addArgument(
|
||||
'template',
|
||||
InputArgument::OPTIONAL,
|
||||
'Twig template file to render (use `-` for STDIN)'
|
||||
)
|
||||
->addOption(
|
||||
'help',
|
||||
'h',
|
||||
InputOption::VALUE_NONE,
|
||||
'Display this usage help'
|
||||
)
|
||||
->addOption(
|
||||
'version',
|
||||
'V',
|
||||
InputOption::VALUE_NONE,
|
||||
'Display version information'
|
||||
)
|
||||
->addOption(
|
||||
'credits',
|
||||
null,
|
||||
InputOption::VALUE_NONE,
|
||||
'Display dependency credits (including Twig version)'
|
||||
)
|
||||
->addOption(
|
||||
'dir',
|
||||
'd',
|
||||
InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY,
|
||||
'Add search directory to loader'
|
||||
)
|
||||
->addOption(
|
||||
'escape',
|
||||
'e',
|
||||
InputOption::VALUE_REQUIRED,
|
||||
'Set autoescape environment option'
|
||||
)
|
||||
->addOption(
|
||||
'json',
|
||||
'j',
|
||||
InputOption::VALUE_REQUIRED,
|
||||
'Pass variables as JSON (dictionary string or file path)'
|
||||
)
|
||||
->addOption(
|
||||
'pair',
|
||||
'p',
|
||||
InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY,
|
||||
'Pass variable as key=value pair'
|
||||
)
|
||||
->addOption(
|
||||
'query',
|
||||
null,
|
||||
InputOption::VALUE_REQUIRED,
|
||||
'Pass variables as URL query string'
|
||||
)
|
||||
->addOption(
|
||||
'strict',
|
||||
's',
|
||||
InputOption::VALUE_NONE,
|
||||
'Enable strict_variables environment option'
|
||||
)
|
||||
;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function execute(InputInterface $input, OutputInterface $output) {
|
||||
switch ( true ) {
|
||||
// Display usage help
|
||||
case $input->getOption('help'):
|
||||
return $this->doHelp($input, $output);
|
||||
// Display version information
|
||||
case $input->getOption('version'):
|
||||
return $this->doVersion($input, $output);
|
||||
// Display package credits
|
||||
case $input->getOption('credits'):
|
||||
return $this->doCredits($input, $output);
|
||||
}
|
||||
// Render Twig template
|
||||
return $this->doRender($input, $output);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*
|
||||
* Overriding this prevents TextDescriptor from displaying the Help section.
|
||||
*/
|
||||
public function getProcessedHelp() {
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Displays usage help.
|
||||
*
|
||||
* @param InputInterface $input
|
||||
* @param OutputInterface $output
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function doHelp(InputInterface $input, OutputInterface $output) {
|
||||
(new DescriptorHelper())->describe($output, $this);
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Displays version information.
|
||||
*
|
||||
* @param InputInterface $input
|
||||
* @param OutputInterface $output
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function doVersion(InputInterface $input, OutputInterface $output) {
|
||||
$nameFmt = '<info>twigc</info>';
|
||||
$versionFmt = '<comment>%s</comment> (<comment>%s</comment> @ <comment>%s</comment>)';
|
||||
|
||||
$output->writeln(sprintf(
|
||||
"${nameFmt} version ${versionFmt}",
|
||||
\Dana\Twigc\Twigc::VERSION_NUMBER,
|
||||
\Dana\Twigc\Twigc::VERSION_COMMIT,
|
||||
\Dana\Twigc\Twigc::VERSION_DATE
|
||||
));
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Displays package credits.
|
||||
*
|
||||
* @param InputInterface $input
|
||||
* @param OutputInterface $output
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function doCredits(InputInterface $input, OutputInterface $output) {
|
||||
$installed = \Dana\Twigc\Twigc::getComposerPackages();
|
||||
|
||||
$table = new Table($output);
|
||||
$table->setStyle('compact');
|
||||
$table->getStyle()->setVerticalBorderChar('');
|
||||
$table->getStyle()->setCellRowContentFormat('%s ');
|
||||
$table->setHeaders(['name', 'version', 'licence']);
|
||||
|
||||
foreach ( $installed as $package ) {
|
||||
$table->addRow([
|
||||
$package->name,
|
||||
ltrim($package->version, 'v'),
|
||||
implode(', ', $package->license) ?: '?',
|
||||
]);
|
||||
}
|
||||
$table->render();
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders a Twig template.
|
||||
*
|
||||
* @param InputInterface $input
|
||||
* @param OutputInterface $output
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function doRender(InputInterface $input, OutputInterface $output) {
|
||||
$inputData = [];
|
||||
$template = $input->getArgument('template');
|
||||
$template = $template === null ? '-' : $template;
|
||||
$dirs = $template === '-' ? [] : [dirname($template)];
|
||||
$dirs = array_merge($dirs, $input->getOption('dir'));
|
||||
$temp = false;
|
||||
$strict = (bool) $input->getOption('strict');
|
||||
$escape = $input->getOption('escape');
|
||||
$inputs = [
|
||||
'json' => (int) ($input->getOption('json') !== null),
|
||||
'pair' => (int) (! empty($input->getOption('pair'))),
|
||||
'query' => (int) ($input->getOption('query') !== null),
|
||||
];
|
||||
|
||||
// If we're reading from STDIN, but STDIN is a TTY, print help and die
|
||||
if ( $template === '-' && posix_isatty(\STDIN) ) {
|
||||
$this->doHelp($input, $output);
|
||||
return 1;
|
||||
}
|
||||
|
||||
// Validate search directories
|
||||
foreach ( $dirs as $dir ) {
|
||||
if ( ! is_dir($dir) ) {
|
||||
throw new \InvalidArgumentException(
|
||||
"Illegal search directory: ${dir}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Normalise auto-escape setting
|
||||
if ( $escape === null ) {
|
||||
$escape = true;
|
||||
} else {
|
||||
$bool = filter_var($escape, \FILTER_VALIDATE_BOOLEAN, \FILTER_NULL_ON_FAILURE);
|
||||
|
||||
if ( $bool !== null ) {
|
||||
$escape = $bool;
|
||||
} else {
|
||||
$escape = strtolower($escape);
|
||||
}
|
||||
}
|
||||
|
||||
// Because Console doesn't allow us to see the order of options supplied
|
||||
// at the command line, there's no good way to sort out the precedence
|
||||
// amongst the different input methods... so let's just say we can only
|
||||
// use one of them at a time
|
||||
if ( array_sum($inputs) > 1 ) {
|
||||
throw new \InvalidArgumentException(
|
||||
'-j, -p, and --query options are mutually exclusive'
|
||||
);
|
||||
}
|
||||
|
||||
// Input data supplied via query string
|
||||
if ( ($query = $input->getOption('query')) !== null ) {
|
||||
if ( $query && $query[0] === '?' ) {
|
||||
$query = substr($query, 1);
|
||||
}
|
||||
parse_str($query, $inputData);
|
||||
|
||||
// Input data supplied via JSON
|
||||
} elseif ( ($json = $input->getOption('json')) !== null ) {
|
||||
$json = trim($json);
|
||||
|
||||
// JSON supplied via STDIN
|
||||
if ( $json === '-' ) {
|
||||
if ( $template === '-' ) {
|
||||
throw new \InvalidArgumentException(
|
||||
'Can not read both template and JSON input from STDIN'
|
||||
);
|
||||
}
|
||||
if ( posix_isatty(\STDIN) ) {
|
||||
throw new \InvalidArgumentException(
|
||||
'Expected JSON input on STDIN'
|
||||
);
|
||||
}
|
||||
$json = file_get_contents('php://stdin');
|
||||
|
||||
// JSON supplied via file
|
||||
} elseif ( $json && $json[0] !== '{' ) {
|
||||
if ( ! file_exists($json) || is_dir($json) ) {
|
||||
throw new \InvalidArgumentException(
|
||||
"Missing or illegal JSON file name: ${json}"
|
||||
);
|
||||
}
|
||||
$json = file_get_contents($json);
|
||||
}
|
||||
|
||||
// This check is here to prevent errors if the input is just empty
|
||||
if ( trim($json) !== '' ) {
|
||||
$inputData = json_decode($json, true);
|
||||
}
|
||||
|
||||
if ( ! is_array($inputData) ) {
|
||||
throw new \InvalidArgumentException(
|
||||
'JSON input must be a dictionary'
|
||||
);
|
||||
}
|
||||
|
||||
// Input data supplied via key=value pair
|
||||
} elseif ( count($input->getOption('pair')) ) {
|
||||
foreach ( $input->getOption('pair') as $pair ) {
|
||||
$kv = explode('=', $pair, 2);
|
||||
|
||||
if ( count($kv) !== 2 ) {
|
||||
throw new \InvalidArgumentException(
|
||||
"Illegal key=value pair: ${pair}"
|
||||
);
|
||||
}
|
||||
|
||||
$inputData[$kv[0]] = $kv[1];
|
||||
}
|
||||
}
|
||||
|
||||
// Validate key names now
|
||||
foreach ( $inputData as $key => $value ) {
|
||||
if ( ! preg_match('#^[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*$#', $key) ) {
|
||||
throw new \InvalidArgumentException(
|
||||
"Illegal variable name: ${key}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Template supplied via STDIN
|
||||
if ( $template === '-' ) {
|
||||
// If we've been supplied one or more search directories, we'll need
|
||||
// to write the template out to a temp directory so we can use the
|
||||
// file-system loader
|
||||
if ( $dirs ) {
|
||||
$temp = true;
|
||||
$template = implode('/', [
|
||||
sys_get_temp_dir(),
|
||||
implode('.', ['twigc', getmypid(), md5(time())]),
|
||||
'-',
|
||||
]);
|
||||
|
||||
mkdir(dirname($template));
|
||||
file_put_contents($template, file_get_contents('php://stdin'), LOCK_EX);
|
||||
|
||||
$dirs = array_merge([dirname($template)], $dirs);
|
||||
|
||||
$loader = new \Twig_Loader_Filesystem($dirs);
|
||||
|
||||
// Otherwise, we can just use the array loader, which is a little
|
||||
// faster and cleaner
|
||||
} else {
|
||||
$loader = new \Twig_Loader_Array([
|
||||
$template => file_get_contents('php://stdin'),
|
||||
]);
|
||||
}
|
||||
|
||||
// Template supplied via file path
|
||||
} else {
|
||||
$loader = new \Twig_Loader_Filesystem($dirs);
|
||||
}
|
||||
|
||||
try {
|
||||
$twig = new \Twig_Environment($loader, [
|
||||
'cache' => false,
|
||||
'debug' => false,
|
||||
'strict_variables' => $strict,
|
||||
'autoescape' => $escape,
|
||||
]);
|
||||
|
||||
$output->writeln(
|
||||
rtrim($twig->render(basename($template), $inputData), "\r\n")
|
||||
);
|
||||
} finally {
|
||||
if ( $temp ) {
|
||||
unlink($template);
|
||||
rmdir(dirname($template));
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
446
src/Twigc/PharCompiler.php
Normal file
446
src/Twigc/PharCompiler.php
Normal file
|
@ -0,0 +1,446 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* This file is part of twigc.
|
||||
*
|
||||
* @author dana geier <dana@dana.is>
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
namespace Dana\Twigc;
|
||||
|
||||
use Symfony\Component\Console\Output\ConsoleOutput;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use Symfony\Component\Finder\Finder;
|
||||
|
||||
/**
|
||||
* Compiles twigc into an executable phar file.
|
||||
*
|
||||
* This clas is heavily inspired by Composer's Compiler:
|
||||
*
|
||||
* Copyright (c) 2016 Nils Adermann, Jordi Boggiano
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in
|
||||
* all copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
* THE SOFTWARE.
|
||||
*/
|
||||
class PharCompiler {
|
||||
protected $output;
|
||||
protected $baseDir;
|
||||
protected $finderSort;
|
||||
protected $versionNumber;
|
||||
protected $versionCommit;
|
||||
protected $versionDate;
|
||||
|
||||
/**
|
||||
* Object constructor.
|
||||
*
|
||||
* @param (bool) $verbose (optional) Whether to display verbose output.
|
||||
*
|
||||
* @return self
|
||||
*/
|
||||
public function __construct($verbose = false) {
|
||||
$this->output = new ConsoleOutput();
|
||||
$this->baseDir = realpath(\Dana\Twigc\Twigc::BASE_DIR);
|
||||
$this->finderSort = function ($a, $b) {
|
||||
return strcmp(
|
||||
strtr($a->getRealPath(), '\\', '/'),
|
||||
strtr($b->getRealPath(), '\\', '/')
|
||||
);
|
||||
};
|
||||
|
||||
if ( $verbose ) {
|
||||
$this->output->setVerbosity(ConsoleOutput::VERBOSITY_VERBOSE);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Compiles the project into an executable phar file.
|
||||
*
|
||||
* @param string $pharFile
|
||||
* (optional) The path (absolute or relative to the CWD) to write the
|
||||
* resulting phar file to.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function compile($pharFile = 'twigc.phar') {
|
||||
$this->output->writeln('Compiling phar...');
|
||||
|
||||
$this->output->writeln('', ConsoleOutput::VERBOSITY_VERBOSE);
|
||||
$this->extractVersionInformation();
|
||||
|
||||
if ( file_exists($pharFile) ) {
|
||||
unlink($pharFile);
|
||||
}
|
||||
|
||||
$phar = new \Phar($pharFile, 0, 'twigc.phar');
|
||||
$phar->setSignatureAlgorithm(\Phar::SHA1);
|
||||
$phar->startBuffering();
|
||||
|
||||
$this->output->writeln('', ConsoleOutput::VERBOSITY_VERBOSE);
|
||||
$this->output->writeln('Adding src files...');
|
||||
$this->addSrc($phar);
|
||||
|
||||
$this->output->writeln('', ConsoleOutput::VERBOSITY_VERBOSE);
|
||||
$this->output->writeln('Adding vendor files...');
|
||||
$this->addVendor($phar);
|
||||
|
||||
$this->output->writeln('', ConsoleOutput::VERBOSITY_VERBOSE);
|
||||
$this->output->writeln('Adding root files...');
|
||||
$this->addRoot($phar);
|
||||
|
||||
$this->output->writeln('', ConsoleOutput::VERBOSITY_VERBOSE);
|
||||
$this->output->writeln('Adding bin files...');
|
||||
$this->addBin($phar);
|
||||
|
||||
$phar->setStub($this->getStub());
|
||||
$phar->stopBuffering();
|
||||
|
||||
unset($phar);
|
||||
|
||||
chmod($pharFile, 0755);
|
||||
|
||||
$this->output->writeln('', ConsoleOutput::VERBOSITY_VERBOSE);
|
||||
$this->output->writeln("Compiled to ${pharFile}.");
|
||||
|
||||
/*
|
||||
// Re-sign the phar with reproducible time stamps and signature
|
||||
$util = new Timestamps($pharFile);
|
||||
$util->updateTimestamps($this->versionDate);
|
||||
$util->save($pharFile, \Phar::SHA1);
|
||||
*/
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts the application version information from the git repository and
|
||||
* sets the associated object properties.
|
||||
*
|
||||
* @return void
|
||||
*
|
||||
* @throws \RuntimeException if `git describe` fails
|
||||
* @throws \RuntimeException if `git log` fails
|
||||
* @throws \RuntimeException if `git log` fails (2)
|
||||
*/
|
||||
protected function extractVersionInformation() {
|
||||
$workDir = escapeshellarg(__DIR__);
|
||||
|
||||
// Get version number
|
||||
$output = [];
|
||||
exec("cd ${workDir} && git describe --tags --match='v*.*.*' --dirty='!'", $output, $ret);
|
||||
|
||||
if ( $ret !== 0 || empty($output) ) {
|
||||
$output = ['0.0.0'];
|
||||
}
|
||||
|
||||
$tokens = explode('-', trim($output[0]));
|
||||
|
||||
$this->versionNumber = rtrim(ltrim($tokens[0], 'v'), '!');
|
||||
|
||||
// If we're ahead of a tag, add the number of commits
|
||||
if ( count($tokens) > 1 ) {
|
||||
$this->versionNumber .= '-plus' . rtrim($tokens[1], '!');
|
||||
}
|
||||
|
||||
// If the index is dirty, add that
|
||||
if ( rtrim(implode('-', $tokens), '!') !== implode('-', $tokens) ) {
|
||||
$this->versionNumber .= '-dirty';
|
||||
}
|
||||
|
||||
// Get version last commit hash
|
||||
$output = [];
|
||||
exec("cd ${workDir} && git log -1 --pretty='%H' HEAD", $output, $ret);
|
||||
|
||||
if ( $ret !== 0 || empty($output) ) {
|
||||
throw new \RuntimeException(
|
||||
'An error occurred whilst running `git log`'
|
||||
);
|
||||
}
|
||||
|
||||
$this->versionCommit = trim($output[0]);
|
||||
|
||||
// Get version last commit date
|
||||
$output = [];
|
||||
exec("cd ${workDir} && git log -1 --pretty='%ci' HEAD", $output, $ret);
|
||||
|
||||
if ( $ret !== 0 || empty($output) ) {
|
||||
throw new \RuntimeException(
|
||||
'An error occurred whilst running `git log`'
|
||||
);
|
||||
}
|
||||
|
||||
$this->versionDate = new \DateTime(trim($output[0]));
|
||||
$this->versionDate->setTimezone(new \DateTimeZone('UTC'));
|
||||
|
||||
if ( $this->output->isVerbose() ) {
|
||||
$this->output->writeln(
|
||||
'Got version number: ' . $this->versionNumber
|
||||
);
|
||||
$this->output->writeln(
|
||||
'Got version commit: ' . $this->versionCommit
|
||||
);
|
||||
$this->output->writeln(
|
||||
'Got version date: ' . $this->versionDate->getTimestamp()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a file to a phar.
|
||||
*
|
||||
* @param \Phar $phar
|
||||
* The phar file to add to.
|
||||
*
|
||||
* @param \SplFileInfo|string $file
|
||||
* The file to add, or its path.
|
||||
*
|
||||
* @param null|bool $strip
|
||||
* (optional) Whether to strip extraneous white space from the file in
|
||||
* order to reduce its size. The default is to auto-detect based on file
|
||||
* extension.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
protected function addFile($phar, $file, $strip = null) {
|
||||
if ( is_string($file) ) {
|
||||
$file = new \SplFileInfo($file);
|
||||
}
|
||||
|
||||
// Strip the absolute base directory off the front of the path
|
||||
$prefix = $this->baseDir . DIRECTORY_SEPARATOR;
|
||||
$path = strtr(
|
||||
str_replace($prefix, '', $file->getRealPath()),
|
||||
'\\',
|
||||
'/'
|
||||
);
|
||||
|
||||
$this->output->writeln("Adding file: ${path}", ConsoleOutput::VERBOSITY_VERBOSE);
|
||||
|
||||
$content = file_get_contents($file);
|
||||
|
||||
// Strip interpreter directives
|
||||
if ( strpos($path, 'bin/') === 0 ) {
|
||||
$content = preg_replace('%^#!/usr/bin/env php\s*%', '', $content);
|
||||
|
||||
// Replace version place-holders
|
||||
} elseif ( $path === 'src/Twigc/Twigc.php' ) {
|
||||
$content = str_replace(
|
||||
[
|
||||
'%version_number%',
|
||||
'%version_commit%',
|
||||
'%version_date%',
|
||||
],
|
||||
[
|
||||
$this->versionNumber,
|
||||
$this->versionCommit,
|
||||
$this->versionDate->format('Y-m-d H:i:s'),
|
||||
],
|
||||
$content
|
||||
);
|
||||
}
|
||||
|
||||
if ( $strip === null ) {
|
||||
$strip = in_array($file->getExtension(), ['json', 'lock', 'php'], true);
|
||||
}
|
||||
|
||||
if ( $strip ) {
|
||||
$content = $this->stripWhiteSpace($content);
|
||||
}
|
||||
|
||||
$phar->addFromString($path, $content);
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes extraneous white space from a string whilst preserving PHP line
|
||||
* numbers.
|
||||
*
|
||||
* @param string $source
|
||||
* The PHP or JSON string to strip white space from.
|
||||
*
|
||||
* @param string $type
|
||||
* (optional) The type of file the string represents. Available options
|
||||
* are 'php' and 'json'. The default is 'php'.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
protected function stripWhiteSpace($source, $type = 'php') {
|
||||
$output = '';
|
||||
|
||||
if ( $type === 'json' ) {
|
||||
$output = json_encode(json_decode($json, true));
|
||||
|
||||
return $output === null ? $source : $output . "\n";
|
||||
}
|
||||
|
||||
if ( ! function_exists('token_get_all') ) {
|
||||
return $source;
|
||||
}
|
||||
|
||||
foreach ( token_get_all($source) as $token ) {
|
||||
// Arbitrary text, return as-is
|
||||
if ( is_string($token) ) {
|
||||
$output .= $token;
|
||||
// Replace comments by empty lines
|
||||
} elseif ( in_array($token[0], [\T_COMMENT, \T_DOC_COMMENT]) ) {
|
||||
$output .= str_repeat("\n", substr_count($token[1], "\n"));
|
||||
// Collapse and normalise white-space
|
||||
} elseif (T_WHITESPACE === $token[0]) {
|
||||
// Collapse consecutive spaces
|
||||
$space = preg_replace('#[ \t]+#', ' ', $token[1]);
|
||||
// Normalise new-lines to \n
|
||||
$space = preg_replace('#(?:\r\n|\r|\n)#', "\n", $space);
|
||||
// Trim leading spaces
|
||||
$space = preg_replace('#\n[ ]+#', "\n", $space);
|
||||
$output .= $space;
|
||||
// Anything else, return as-is
|
||||
} else {
|
||||
$output .= $token[1];
|
||||
}
|
||||
}
|
||||
|
||||
return $output;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds files from src directory to a phar.
|
||||
*
|
||||
* @param \Phar $phar The phar file to add to.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
protected function addSrc($phar) {
|
||||
$finder = new Finder();
|
||||
$finder
|
||||
->files()
|
||||
->in($this->baseDir . '/src')
|
||||
->ignoreDotFiles(true)
|
||||
->ignoreVCS(true)
|
||||
->name('*.php')
|
||||
->notName('PharCompiler.php')
|
||||
->sort($this->finderSort)
|
||||
;
|
||||
|
||||
foreach ( $finder as $file ) {
|
||||
$this->addFile($phar, $file);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds files from vendor directory to a phar.
|
||||
*
|
||||
* @param \Phar $phar The phar file to add to.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
protected function addVendor($phar) {
|
||||
$devPaths = \Dana\Twigc\Twigc::getComposerDevPackages();
|
||||
$devPaths = array_map(function ($x) {
|
||||
return $x->name . '/';
|
||||
}, $devPaths);
|
||||
|
||||
$finder = new Finder();
|
||||
$finder
|
||||
->files()
|
||||
->in($this->baseDir . '/vendor')
|
||||
->ignoreDotFiles(true)
|
||||
->ignoreVCS(true)
|
||||
;
|
||||
|
||||
// Exclude files from dev packages
|
||||
foreach ( $devPaths as $path ) {
|
||||
$finder->notPath($path);
|
||||
}
|
||||
|
||||
$finder
|
||||
->exclude('bin')
|
||||
->exclude('doc')
|
||||
->exclude('docs')
|
||||
->exclude('test')
|
||||
->exclude('tests')
|
||||
->exclude('Test')
|
||||
->exclude('Tests')
|
||||
->notName('*.c')
|
||||
->notName('*.h')
|
||||
->notName('*.m4')
|
||||
->notName('*.w32')
|
||||
->notName('*.xml.dist')
|
||||
->notName('build.xml')
|
||||
->notName('composer.json')
|
||||
->notName('composer.lock')
|
||||
->notName('travis-ci.xml')
|
||||
->notName('phpunit.xml')
|
||||
->notName('ChangeLog*')
|
||||
->notName('CHANGE*')
|
||||
->notName('*CONDUCT*')
|
||||
->notName('CONTRIBUT*')
|
||||
->notName('README*')
|
||||
->sort($this->finderSort)
|
||||
;
|
||||
|
||||
foreach ( $finder as $file ) {
|
||||
$this->addFile($phar, $file);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds files from project root directory to a phar.
|
||||
*
|
||||
* @param \Phar $phar The phar file to add to.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
protected function addRoot($phar) {
|
||||
$this->addFile($phar, $this->baseDir . '/composer.json');
|
||||
$this->addFile($phar, $this->baseDir . '/composer.lock');
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds files from bin directory to a phar.
|
||||
*
|
||||
* @param \Phar $phar The phar file to add to.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
protected function addBin($phar) {
|
||||
$this->addFile($phar, $this->baseDir . '/bin/twigc');
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the phar stub.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
protected function getStub() {
|
||||
$stub = "
|
||||
#!/usr/bin/env php
|
||||
<?php
|
||||
|
||||
/**
|
||||
* This file is part of twigc.
|
||||
*
|
||||
* @author dana geier <dana@dana.is>
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
\Phar::mapPhar('twigc.phar');
|
||||
require 'phar://twigc.phar/bin/twigc';
|
||||
__HALT_COMPILER();
|
||||
";
|
||||
|
||||
return str_replace("\t", '', trim($stub)) . "\n";
|
||||
}
|
||||
}
|
||||
|
88
src/Twigc/Twigc.php
Normal file
88
src/Twigc/Twigc.php
Normal file
|
@ -0,0 +1,88 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* This file is part of twigc.
|
||||
*
|
||||
* @author dana geier <dana@dana.is>
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
namespace Dana\Twigc;
|
||||
|
||||
/**
|
||||
* Holds various project-specific constants and methods.
|
||||
*/
|
||||
class Twigc {
|
||||
const BASE_DIR = __DIR__ . '/../..';
|
||||
const VERSION_NUMBER = '%version_number%';
|
||||
const VERSION_COMMIT = '%version_commit%';
|
||||
const VERSION_DATE = '%version_date%';
|
||||
|
||||
/**
|
||||
* Returns an array of data representing the project's Composer lock file.
|
||||
*
|
||||
* @return array
|
||||
*
|
||||
* @throws \RuntimeException if composer.lock doesn't exist
|
||||
* @throws \RuntimeException if composer.lock can't be decoded
|
||||
*/
|
||||
private static function parseComposerLock() {
|
||||
$lockFile = static::BASE_DIR . '/composer.lock';
|
||||
|
||||
if ( ! file_exists($lockFile) ) {
|
||||
throw new \RuntimeException('Missing ' . basename($lockFile));
|
||||
}
|
||||
|
||||
$installed = json_decode(file_get_contents($lockFile), true);
|
||||
|
||||
if ( empty($installed) || ! isset($installed['packages']) ) {
|
||||
throw new \RuntimeException('Error decoding ' . basename($lockFile));
|
||||
}
|
||||
|
||||
return $installed;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sorts and object-ifies an array of package data.
|
||||
*
|
||||
* @param array $packages Package data from composer.lock.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
private static function massagePackages(array $packages) {
|
||||
usort($packages, function ($a, $b) {
|
||||
return strcasecmp($a['name'], $b['name']);
|
||||
});
|
||||
|
||||
foreach ( $packages as &$package ) {
|
||||
$package = (object) $package;
|
||||
}
|
||||
|
||||
return $packages;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an array of installed non-dev Composer packages based on the
|
||||
* project's Composer lock file.
|
||||
*
|
||||
* @return object[] An array of objects representing Composer packages.
|
||||
*/
|
||||
public static function getComposerPackages() {
|
||||
$packages = static::parseComposerLock()['packages'];
|
||||
|
||||
return static::massagePackages($packages);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an array of installed dev Composer packages based on the
|
||||
* project's lock file.
|
||||
*
|
||||
* @return object[] An array of objects representing Composer packages.
|
||||
*/
|
||||
public static function getComposerDevPackages() {
|
||||
$packages = static::parseComposerLock()['packages-dev'];
|
||||
|
||||
return static::massagePackages($packages);
|
||||
}
|
||||
}
|
||||
|
47
src/bootstrap.php
Normal file
47
src/bootstrap.php
Normal file
|
@ -0,0 +1,47 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* This file is part of twigc.
|
||||
*
|
||||
* @author dana geier <dana@dana.is>
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
/**
|
||||
* Helper function for printing an error message.
|
||||
*
|
||||
* Uses fprintf() to print to STDERR if available; uses echo otherwise.
|
||||
*
|
||||
* @param string $string
|
||||
* (optional) The message to print. Will be passed through rtrim(); if the
|
||||
* result is an empty string, only an empty line will be printed; otherwise,
|
||||
* the text 'twigc: ' will be appended to the beginning.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
function twigc_puts_error($string = '') {
|
||||
$string = rtrim($string);
|
||||
$string = $string === '' ? '' : "twigc: ${string}";
|
||||
|
||||
// \STDERR only exists when we're using the CLI SAPI
|
||||
if ( defined('\\STDERR') ) {
|
||||
fprintf(\STDERR, "%s\n", $string);
|
||||
} else {
|
||||
echo $string, "\n";
|
||||
}
|
||||
}
|
||||
|
||||
// Disallow running from non-CLI SAPIs
|
||||
if ( \PHP_SAPI !== 'cli' ) {
|
||||
twigc_puts_error("This tool must be invoked via PHP's CLI SAPI.");
|
||||
exit(1);
|
||||
}
|
||||
|
||||
// Give a meaningful error if we don't have our vendor dependencies
|
||||
if ( ! file_exists(__DIR__ . '/../vendor/autoload.php') ) {
|
||||
twigc_puts_error('Auto-loader is missing — try running `composer install`.');
|
||||
exit(1);
|
||||
}
|
||||
|
||||
require_once __DIR__ . '/../vendor/autoload.php';
|
||||
|
Loading…
Reference in a new issue