src/Shell/Command/DocumentationCommand.php
<?php
namespace Strata\Shell\Command;
use Strata\Strata;
use Strata\Shell\Command\StrataCommandBase;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Input\InputDefinition;
use Symfony\Component\Console\Output\OutputInterface;
/**
* Automates Strata's Documentation.
*
* Intended use:
* <code>bin/strata document</code>
*/
class DocumentationCommand extends StrataCommandBase
{
/**
* {@inheritdoc}
*/
protected function configure()
{
$this
->setName('document')
->setDescription('Documents the current app')
->setDefinition(
new InputDefinition(array(
new InputOption('destination', 'c', InputOption::VALUE_OPTIONAL),
))
);
}
/**
* {@inheritdoc}
*/
protected function execute(InputInterface $input, OutputInterface $output)
{
$this->startup($input, $output);
$this->deletePrevious();
$this->generateAPI();
$this->nl();
$this->generateThemesApi();
$this->nl();
$this->generateThemesDocumentation();
$this->nl();
$this->shutdown();
}
/**
* Gets the documentation's destination, either from an
* argument passed as option or from the default path.
* @return string Destination path.
*/
protected function getDestination()
{
if (!is_null($this->input->getOption('destination'))) {
return $this->input->getOption('destination');
}
return implode(DIRECTORY_SEPARATOR, array(Strata::getRootPath(), "doc", DIRECTORY_SEPARATOR));
}
/**
* Gets the api documentation's destination
* @return string Destination path.
*/
protected function getApiDestination()
{
return $this->getDestination() . 'api';
}
/**
* Gets the Wordpress themes documentation's destination
* @return string Destination path.
*/
protected function getWpdocDestination()
{
return $this->getDestination() . 'wpdoc';
}
/**
* Gets the Wordpress themes API documentation's destination
* @return string Destination path.
*/
protected function getWpApiDestination()
{
return $this->getDestination() . 'wpapi';
}
/**
* Deletes the previous generated output in the destination folders.
* @return [type] [description]
*/
protected function deletePrevious()
{
$this->rrmdir($this->getApiDestination());
$this->rrmdir($this->getWpApiDestination());
$this->rrmdir($this->getWpdocDestination());
}
/**
* Generates the API documentation contents
* @return null
*/
protected function generateAPI()
{
$srcPath = Strata::getSRCPath();
$vendorPath = Strata::getVendorPath();
$tmpPath = Strata::getTmpPath();
$this->output->writeLn("<info>Generating API</info>");
$this->output->writeLn($this->tree(true) . "Scanning $srcPath");
$this->nl();
if (!file_exists($tmpPath . "phploc.xml")) {
touch($tmpPath . "phploc.xml");
}
if (!file_exists($tmpPath . "phpcs.xml")) {
touch($tmpPath . "phpcs.xml");
}
system(sprintf("%sbin/phploc --log-xml %sphploc.xml test", $vendorPath, $tmpPath));
system(sprintf("%sbin/phpcs src --standard=PSR2 --report-xml=%sphpcs.xml", $vendorPath, $tmpPath));
system(sprintf("%sbin/phpdox", $vendorPath));
}
/**
* Generates the API documentation contents
* @return null
*/
protected function generateThemesAPI()
{
// $themesPath = Strata::getThemesPath();
// $this->output->writeLn("<info>Generating Wordpress theme API</info>");
// $this->output->writeLn($this->tree(true) . "Scanning $themesPath");
// $this->nl();
// system(sprintf("%s -d %s -t %s", $this->getPhpDocumentorBin(), $themesPath, $this->getWpApiDestination()));
}
/**
* Generates the Wordpress themes documentation contents
* @return null
*/
protected function generateThemesDocumentation()
{
$this->output->writeLn("<info>Generating Wordpress theme documentation</info>");
$themesPath = Strata::getThemesPath();
$info = $this->scanThemeDirectories($themesPath);
$this->writeThemesDocumentation($info);
}
/**
* Render the theme documentation file based on known $info fields.
* @todo This will do for now, but the template shouldn't be hardcoded.
* @param array $info The parsed theme data
* @return bool True is the file was successfully created.
*/
protected function writeThemesDocumentation($info)
{
$header = '<!DOCTYPE html><html><head><meta charset="utf-8"><title>Overview</title>';
$header .= '<link rel="stylesheet" href="../api/resources/style.css">';
$header .= '</head><body><div id="content">';
$content = '<h1>Project snapshot</h1>';
$footer = '</div></body></html>';
$htmlTpl = "<tr><td class=\"attributes\"><strong>%s</strong></td><td class=\"name\"><div><code>%s</code><div class=\"description short\">%s</div></div></td></tr>";
foreach ($info["themes"] as $themeName => $theme) {
$content .= "<h2>$themeName</h2>";
$content .= '<table class="summary" id="templates" style="width:45%; float:left;">';
$content .= "<caption>This theme defines " . count($theme["templates"]) . " template files.</caption><tbody>";
foreach ($theme["templates"] as $template) {
$content .= sprintf($htmlTpl,
empty($template["Template Name"]) ? 'Name not specified' : $template["Template Name"],
$template["filename"],
empty($template["Description"]) ? 'Missing description.' : $template["Description"]
);
}
$content .= "</tbody></table>";
$content .= '<table class="summary" id="libs" style="width:45%; margin-left:2%; float:left;">';
$content .= "<caption>This theme uses " . count($theme["libs"]) . " obvious library files.</caption><tbody>";
foreach ($theme["libs"] as $lib) {
$content .= sprintf($htmlTpl,
empty($lib["Name"]) ? 'Name not specified' : $lib["Name"],
$lib["filename"],
empty($lib["Description"]) ? 'Missing description.' : $lib["Description"]
);
}
$content .= "</tbody></table>";
}
$wpdocdir = $this->getWpdocDestination();
if (!is_dir($wpdocdir)) {
mkdir($wpdocdir);
}
return file_put_contents($wpdocdir . "/index.html", $header . $content . $footer, LOCK_EX);
}
/**
* Scans the theme directories of the current Wordpress installation and looks
* for things useful for a programmer.
* @param string $base The base theme path.
* @return array A associative array of theme information
*/
protected function scanThemeDirectories($base)
{
$tree = array("themes" => array());
$di = new \RecursiveDirectoryIterator($base);
foreach (new \RecursiveIteratorIterator($di) as $filename => $file) {
// Obtain the theme scope, add it to the stack if it wasn't
// saved yet.
$theme = null;
if (preg_match("/themes\/(.+?)\//", $filename, $matches)) {
$theme = $matches[1];
if (!array_key_exists($theme, $tree["themes"])) {
$tree["themes"][$theme] = array(
"templates" => array(),
"libs" => array()
);
$this->output->writeLn($this->tree(true) . "Scanning $theme");
}
}
// Match for wordpress templates
if (preg_match("/themes\/$theme\/template\-(.+?)\.php/", $filename, $matches)) {
$template = $matches[1];
if (!array_key_exists($template, $tree["themes"][$theme]["templates"])) {
$tree["themes"][$theme]["templates"][$template] = array();
$headerKeys = array("Template Name" => "Template Name", "Description" => "Description");
$templateDetails = $this->getFileData($filename, $headerKeys);
$tree["themes"][$theme]["templates"][$template]["filename"] = $filename;
foreach ($headerKeys as $key) {
$tree["themes"][$theme]["templates"][$template][$key] = $templateDetails[$key];
}
}
}
// Match for custom lib files
if (preg_match("/themes\/$theme\/lib\/(.+?)\.php/", $filename, $matches)) {
$lib = $matches[1];
if (!array_key_exists($lib, $tree["themes"][$theme]["libs"])) {
$tree["themes"][$theme]["libs"][$lib] = array();
$headerKeys = array("Name" => "Name", "Description" => "Description");
$libDetails = $this->getFileData($filename, $headerKeys);
$tree["themes"][$theme]["libs"][$lib]["filename"] = $filename;
foreach ($headerKeys as $key) {
$tree["themes"][$theme]["libs"][$lib][$key] = $libDetails[$key];
}
}
}
}
return $tree;
}
/**
* This is a lightly modified copy of Wordpress get_file_data() found in wp-includes/functions.php.
* Because we don't load Wordpress when executing Strata Shell, we don't have access to that function.
* @param string $file
* @param array $default_headers
* @param string $context
* @return array
*/
protected function getFileData($file, $all_headers = array(), $context = '')
{
// We don't need to write to the file, so just open for reading.
$fp = fopen( $file, 'r' );
// Pull only the first 8kiB of the file in.
$file_data = fread( $fp, 8192 );
// PHP will close file handle, but we are good citizens.
fclose( $fp );
// Make sure we catch CR-only line endings.
$file_data = str_replace( "\r", "\n", $file_data );
/**
* Filter extra file headers by context.
*
* The dynamic portion of the hook name, `$context`, refers to
* the context where extra headers might be loaded.
*
* @since 2.9.0
*
* @param array $extra_context_headers Empty array by default.
*/
foreach ( $all_headers as $field => $regex ) {
if ( preg_match( '/^[ \t\/*#@]*' . preg_quote( $regex, '/' ) . ':(.*)$/mi', $file_data, $match ) && $match[1] )
$all_headers[ $field ] = $match[1];
else
$all_headers[ $field ] = '';
}
return $all_headers;
}
/**
* Recurses through a directory and deletes sub-directories.
* @param string $dir A directory path to delete
* @return null
*/
protected function rrmdir($dir)
{
if (is_dir($dir)) {
$objects = scandir($dir);
foreach ($objects as $object) {
if ($object != "." && $object != "..") {
if (filetype($dir."/".$object) == "dir") $this->rrmdir($dir."/".$object); else unlink($dir."/".$object);
}
}
reset($objects);
rmdir($dir);
}
}
}