Location: PHPKode > scripts > ApiGen > apigen/ApiGen/Config.php
<?php

/**
 * ApiGen 2.8.0 - API documentation generator for PHP 5.3+
 *
 * Copyright (c) 2010-2011 David Grudl (http://davidgrudl.com)
 * Copyright (c) 2011-2012 Jaroslav Hanslík (https://github.com/kukulich)
 * Copyright (c) 2011-2012 Ondřej Nešpor (https://github.com/Andrewsville)
 *
 * For the full copyright and license information, please view
 * the file LICENSE.md that was distributed with this source code.
 */

namespace ApiGen;

use Nette\Utils\Neon;

/**
 * Configuration processing class.
 */
class Config
{
	/**
	 * Options.
	 *
	 * @var array
	 */
	private $options = array();

	/**
	 * Parsed configuration.
	 *
	 * @var array
	 */
	private $config = array();

	/**
	 * Default configuration.
	 *
	 * @var array
	 */
	private static $defaultConfig = array(
		'config' => '',
		'source' => array(),
		'destination' => '',
		'extensions' => array('php'),
		'exclude' => array(),
		'skipDocPath' => array(),
		'skipDocPrefix' => array(),
		'charset' => array('auto'),
		'main' => '',
		'title' => '',
		'baseUrl' => '',
		'googleCseId' => '',
		'googleCseLabel' => '',
		'googleAnalytics' => '',
		'templateConfig' => '',
		'allowedHtml' => array('b', 'i', 'a', 'ul', 'ol', 'li', 'p', 'br', 'var', 'samp', 'kbd', 'tt'),
		'groups' => 'auto',
		'autocomplete' => array('classes', 'constants', 'functions'),
		'accessLevels' => array('public', 'protected'),
		'internal' => false,
		'php' => true,
		'tree' => true,
		'deprecated' => false,
		'todo' => false,
		'download' => false,
		'sourceCode' => true,
		'report' => '',
		'undocumented' => '',
		'wipeout' => true,
		'quiet' => false,
		'progressbar' => true,
		'colors' => true,
		'updateCheck' => true,
		'debug' => false
	);

	/**
	 * File or directory path options.
	 *
	 * @var array
	 */
	private static $pathOptions = array(
		'config',
		'source',
		'destination',
		'templateConfig',
		'report'
	);

	/**
	 * Possible values for options.
	 *
	 * @var array
	 */
	private static $possibleOptionsValues = array(
		'groups' => array('auto', 'namespaces', 'packages', 'none'),
		'autocomplete' => array('classes', 'constants', 'functions', 'methods', 'properties', 'classconstants'),
		'accessLevels' => array('public', 'protected', 'private')
	);

	/**
	 * Initializes default configuration.
	 */
	public function __construct()
	{
		$templateDir = self::isInstalledByPear() ? '@data_dir@' . DIRECTORY_SEPARATOR . 'ApiGen' : realpath(__DIR__ . DIRECTORY_SEPARATOR . '..');
		self::$defaultConfig['templateConfig'] = $templateDir . DIRECTORY_SEPARATOR . 'templates' . DIRECTORY_SEPARATOR . 'default' . DIRECTORY_SEPARATOR . 'config.neon';
		self::$defaultConfig['colors'] = 'WIN' !== substr(PHP_OS, 0, 3);
		$this->config = self::$defaultConfig;
	}

	/**
	 * Processes command line options.
	 *
	 * @param array $options
	 * @return \ApiGen\Config
	 */
	public function processCliOptions(array $options)
	{
		while ($option = current($options)) {
			if (preg_match('~^--([a-z][-a-z]*[a-z])(?:=(.+))?$~', $option, $matches) || preg_match('~^-([a-z])=?(.*)~', $option, $matches)) {
				$name = $matches[1];

				if (!empty($matches[2])) {
					$value = $matches[2];
				} else {
					$next = next($options);
					if (false === $next || '-' === $next{0}) {
						prev($options);
						$value = '';
					} else {
						$value = $next;
					}
				}

				$this->options[$name][] = $value;
			}

			next($options);
		}
		$this->options = array_map(function($value) {
			return 1 === count($value) ? $value[0] : $value;
		}, $this->options);

		// Compatibility with ApiGen 1.0
		foreach (array('config', 'source', 'destination') as $option) {
			if (isset($this->options[$option{0}]) && !isset($this->options[$option])) {
				$this->options[$option] = $this->options[$option{0}];
			}
			unset($this->options[$option{0}]);
		}

		return $this;
	}

	/**
	 * Prepares configuration.
	 *
	 * @return \ApiGen\Config
	 * @throws \ApiGen\ConfigException If something in configuration is wrong.
	 */
	public function prepare()
	{
		// Command line options
		$cli = array();
		$translator = array();
		foreach ($this->options as $option => $value) {
			$converted = preg_replace_callback('~-([a-z])~', function($matches) {
				return strtoupper($matches[1]);
			}, $option);

			$cli[$converted] = $value;
			$translator[$converted] = $option;
		}

		$unknownOptions = array_keys(array_diff_key($cli, self::$defaultConfig));
		if (!empty($unknownOptions)) {
			$originalOptions = array_map(function($option) {
				return (1 === strlen($option) ? '-' : '--') . $option;
			}, array_values(array_diff_key($translator, self::$defaultConfig)));

			$message = count($unknownOptions) > 1
				? sprintf('Unknown command line options "%s"', implode('", "', $originalOptions))
				: sprintf('Unknown command line option "%s"', $originalOptions[0]);
			throw new ConfigException($message);
		}

		// Config file
		$neon = array();
		if (empty($this->options) && $this->defaultConfigExists()) {
			$this->options['config'] = $this->getDefaultConfigPath();
		}
		if (isset($this->options['config']) && is_file($this->options['config'])) {
			$neon = Neon::decode(file_get_contents($this->options['config']));
			foreach (self::$pathOptions as $option) {
				if (!empty($neon[$option])) {
					if (is_array($neon[$option])) {
						foreach ($neon[$option] as $key => $value) {
							$neon[$option][$key] = $this->getAbsolutePath($value);
						}
					} else {
						$neon[$option] = $this->getAbsolutePath($neon[$option]);
					}
				}
			}

			$unknownOptions = array_keys(array_diff_key($neon, self::$defaultConfig));
			if (!empty($unknownOptions)) {
				$message = count($unknownOptions) > 1
					? sprintf('Unknown config file options "%s"', implode('", "', $unknownOptions))
					: sprintf('Unknown config file option "%s"', $unknownOptions[0]);
				throw new ConfigException($message);
			}
		}

		// Merge options
		$this->config = array_merge(self::$defaultConfig, $neon, $cli);

		// Compatibility with old option name "undocumented"
		if (!isset($this->config['report']) && isset($this->config['undocumented'])) {
			$this->config['report'] = $this->config['undocumented'];
			unset($this->config['undocumented']);
		}

		foreach (self::$defaultConfig as $option => $valueDefinition) {
			if (is_array($this->config[$option]) && !is_array($valueDefinition)) {
				throw new ConfigException(sprintf('Option "%s" must be set only once', $option));
			}

			if (is_bool($this->config[$option]) && !is_bool($valueDefinition)) {
				throw new ConfigException(sprintf('Option "%s" expects value', $option));
			}

			if (is_bool($valueDefinition) && !is_bool($this->config[$option])) {
				// Boolean option
				$value = strtolower($this->config[$option]);
				if ('on' === $value || 'yes' === $value || 'true' === $value || '' === $value) {
					$value = true;
				} elseif ('off' === $value || 'no' === $value || 'false' === $value) {
					$value = false;
				}
				$this->config[$option] = (bool) $value;
			} elseif (is_array($valueDefinition)) {
				// Array option
				$this->config[$option] = array_unique((array) $this->config[$option]);
				foreach ($this->config[$option] as $key => $value) {
					$value = explode(',', $value);
					while (count($value) > 1) {
						array_push($this->config[$option], array_shift($value));
					}
					$this->config[$option][$key] = array_shift($value);
				}
				$this->config[$option] = array_filter($this->config[$option]);
			}

			// Check posssible values
			if (!empty(self::$possibleOptionsValues[$option])) {
				$values = self::$possibleOptionsValues[$option];

				if (is_array($valueDefinition)) {
					$this->config[$option] = array_filter($this->config[$option], function($value) use ($values) {
						return in_array($value, $values);
					});
				} elseif (!in_array($this->config[$option], $values)) {
					$this->config[$option] = '';
				}
			}
		}

		// Unify character sets
		$this->config['charset'] = array_map('strtoupper', $this->config['charset']);

		// Process options that specify a filesystem path
		foreach (self::$pathOptions as $option) {
			if (is_array($this->config[$option])) {
				array_walk($this->config[$option], function(&$value) {
					if (file_exists($value)) {
						$value = realpath($value);
					}
				});
				usort($this->config[$option], 'strcasecmp');
			} else {
				if (file_exists($this->config[$option])) {
					$this->config[$option] = realpath($this->config[$option]);
				}
			}
		}

		// Unify directory separators
		foreach (array('exclude', 'skipDocPath') as $option) {
			$this->config[$option] = array_map(function($mask) {
				return str_replace(array('/', '\\'), DIRECTORY_SEPARATOR, $mask);
			}, $this->config[$option]);
			usort($this->config[$option], 'strcasecmp');
		}

		// Unify prefixes
		$this->config['skipDocPrefix'] = array_map(function($prefix) {
			return ltrim($prefix, '\\');
		}, $this->config['skipDocPrefix']);
		usort($this->config['skipDocPrefix'], 'strcasecmp');

		// Base url without slash at the end
		$this->config['baseUrl'] = rtrim($this->config['baseUrl'], '/');

		// No progressbar in quiet mode
		if ($this->config['quiet']) {
			$this->config['progressbar'] = false;
		}

		// Check
		$this->check();

		// Default template config
		$this->config['template'] = array(
			'require' => array(),
			'resources' => array(),
			'templates' => array(
				'common' => array(),
				'optional' => array()
			)
		);

		// Merge template config
		$this->config = array_merge_recursive($this->config, array('template' => Neon::decode(file_get_contents($fileName = $this->config['templateConfig']))));
		$this->config['template']['config'] = realpath($fileName);

		// Check template
		$this->checkTemplate();

		return $this;
	}

	/**
	 * Checks configuration.
	 *
	 * @return \ApiGen\Config
	 * @throws \ApiGen\ConfigException If something in configuration is wrong.
	 */
	private function check()
	{
		if (!empty($this->config['config']) && !is_file($this->config['config'])) {
			throw new ConfigException(sprintf('Config file "%s" doesn\'t exist', $this->config['config']));
		}

		if (empty($this->config['source'])) {
			throw new ConfigException('Source is not set');
		}
		foreach ($this->config['source'] as $source) {
			if (!file_exists($source)) {
				throw new ConfigException(sprintf('Source "%s" doesn\'t exist', $source));
			}
		}

		if (empty($this->config['destination'])) {
			throw new ConfigException('Destination is not set');
		}

		foreach ($this->config['extensions'] as $extension) {
			if (!preg_match('~^[a-z\\d]+$~i', $extension)) {
				throw new ConfigException(sprintf('Invalid file extension "%s"', $extension));
			}
		}

		if (!is_file($this->config['templateConfig'])) {
			throw new ConfigException(sprintf('Template config "%s" doesn\'t exist', $this->config['templateConfig']));
		}

		if (!empty($this->config['baseUrl']) && !preg_match('~^https?://(?:[-a-z0-9]+\.)+[a-z]{2,6}(?:/.*)?$~i', $this->config['baseUrl'])) {
			throw new ConfigException(sprintf('Invalid base url "%s"', $this->config['baseUrl']));
		}

		if (!empty($this->config['googleCseId']) && !preg_match('~^\d{21}:[-a-z0-9_]{11}$~', $this->config['googleCseId'])) {
			throw new ConfigException(sprintf('Invalid Google Custom Search ID "%s"', $this->config['googleCseId']));
		}

		if (!empty($this->config['googleAnalytics']) && !preg_match('~^UA\\-\\d+\\-\\d+$~', $this->config['googleAnalytics'])) {
			throw new ConfigException(sprintf('Invalid Google Analytics tracking code "%s"', $this->config['googleAnalytics']));
		}

		if (empty($this->config['groups'])) {
			throw new ConfigException('No supported groups value given');
		}

		if (empty($this->config['autocomplete'])) {
			throw new ConfigException('No supported autocomplete value given');
		}

		if (empty($this->config['accessLevels'])) {
			throw new ConfigException('No supported access level given');
		}

		return $this;
	}

	/**
	 * Checks template configuration.
	 *
	 * @return \ApiGen\Config
	 * @throws \ApiGen\ConfigException If something in template configuration is wrong.
	 */
	private function checkTemplate()
	{
		$require = $this->config['template']['require'];
		if (isset($require['min']) && !preg_match('~^\\d+(?:\\.\\d+){0,2}$~', $require['min'])) {
			throw new ConfigException(sprintf('Invalid minimal version definition "%s"', $require['min']));
		}
		if (isset($require['max']) && !preg_match('~^\\d+(?:\\.\\d+){0,2}$~', $require['max'])) {
			throw new ConfigException(sprintf('Invalid maximal version definition "%s"', $require['max']));
		}

		$isMinOk = function($min) {
			$min .= str_repeat('.0', 2 - substr_count($min, '.'));
			return version_compare($min, Generator::VERSION, '<=');
		};
		$isMaxOk = function($max) {
			$max .= str_repeat('.0', 2 - substr_count($max, '.'));
			return version_compare($max, Generator::VERSION, '>=');
		};

		if (isset($require['min'], $require['max']) && (!$isMinOk($require['min']) || !$isMaxOk($require['max']))) {
			throw new ConfigException(sprintf('The template requires version from "%s" to "%s", you are using version "%s"', $require['min'], $require['max'], Generator::VERSION));
		} elseif (isset($require['min']) && !$isMinOk($require['min'])) {
			throw new ConfigException(sprintf('The template requires version "%s" or newer, you are using version "%s"', $require['min'], Generator::VERSION));
		} elseif (isset($require['max']) && !$isMaxOk($require['max'])) {
			throw new ConfigException(sprintf('The template requires version "%s" or older, you are using version "%s"', $require['max'], Generator::VERSION));
		}

		foreach (array('main', 'optional') as $section) {
			foreach ($this->config['template']['templates'][$section] as $type => $config) {
				if (!isset($config['filename'])) {
					throw new ConfigException(sprintf('Filename for "%s" is not defined', $type));
				}
				if (!isset($config['template'])) {
					throw new ConfigException(sprintf('Template for "%s" is not defined', $type));
				}
				if (!is_file(dirname($this->config['templateConfig']) . DIRECTORY_SEPARATOR . $config['template'])) {
					throw new ConfigException(sprintf('Template for "%s" doesn\'t exist', $type));
				}
			}
		}

		return $this;
	}

	/**
	 * Returns default configuration file path.
	 *
	 * @return string
	 */
	private function getDefaultConfigPath()
	{
		return getcwd() . DIRECTORY_SEPARATOR . 'apigen.neon';
	}

	/**
	 * Checks if default configuration file exists.
	 *
	 * @return boolean
	 */
	private function defaultConfigExists()
	{
		return is_file($this->getDefaultConfigPath());
	}

	/**
	 * Returns absolute path.
	 *
	 * @param string $path Path
	 * @return string
	 */
	private function getAbsolutePath($path)
	{
		if (preg_match('~/|[a-z]:~Ai', $path)) {
			return $path;
		}

		return dirname($this->options['config']) . DIRECTORY_SEPARATOR . $path;
	}

	/**
	 * Checks if a configuration option exists.
	 *
	 * @param string $name Option name
	 * @return boolean
	 */
	public function __isset($name)
	{
		return isset($this->config[$name]);
	}

	/**
	 * Returns a configuration option value.
	 *
	 * @param string $name Option name
	 * @return mixed
	 */
	public function __get($name)
	{
		return isset($this->config[$name]) ? $this->config[$name] : null;
	}

	/**
	 * If the user requests help.
	 *
	 * @return boolean
	 */
	public function isHelpRequested()
	{
		if (empty($this->options) && !$this->defaultConfigExists()) {
			return true;
		}

		if (isset($this->options['h']) || isset($this->options['help'])) {
			return true;
		}

		return false;
	}

	/**
	 * Returns help.
	 *
	 * @return string
	 */
	public function getHelp()
	{
		return <<<"HELP"
Usage:
	apigen @hide@address.com@c <@hide@address.com@c> [options]
	apigen @hide@address.com@c <@hide@address.com@c|@hide@address.com@c> @hide@address.com@c <@hide@address.com@c> [options]

Options:
	@hide@address.com@c|@hide@address.com@c        <@hide@address.com@c>      Config file
	@hide@address.com@c|@hide@address.com@c        <@hide@address.com@c|@hide@address.com@c>  Source file or directory to parse (can be used multiple times)
	@hide@address.com@c|@hide@address.com@c   <@hide@address.com@c>       Directory where to save the generated documentation
	@hide@address.com@c       <@hide@address.com@c>      List of allowed file extensions, default "@hide@address.com@c"
	@hide@address.com@c          <@hide@address.com@c>      Mask (case sensitive) to exclude file or directory from processing (can be used multiple times)
	@hide@address.com@c    <@hide@address.com@c>      Don't generate documentation for elements from file or directory with this (case sensitive) mask (can be used multiple times)
	@hide@address.com@c  <@hide@address.com@c>     Don't generate documentation for elements with this (case sensitive) name prefix (can be used multiple times)
	@hide@address.com@c          <@hide@address.com@c>      Character set of source files, default "@hide@address.com@c"
	@hide@address.com@c             <@hide@address.com@c>     Main project name prefix
	@hide@address.com@c            <@hide@address.com@c>     Title of generated documentation
	@hide@address.com@c         <@hide@address.com@c>     Documentation base URL
	@hide@address.com@c    <@hide@address.com@c>     Google Custom Search ID
	@hide@address.com@c <@hide@address.com@c>     Google Custom Search label
	@hide@address.com@c <@hide@address.com@c>     Google Analytics tracking code
	@hide@address.com@c  <@hide@address.com@c>      Template config file, default "@value@{$this->config['templateConfig']}@c"
	@hide@address.com@c     <@hide@address.com@c>      List of allowed HTML tags in documentation, default "@hide@address.com,i,a,ul,ol,li,p,br,var,samp,kbd,hide@address.com"
	@hide@address.com@c           <@hide@address.com@c>     How should elements be grouped in the menu. Default value is "@hide@address.com@c" (namespaces if available, packages otherwise)
	@hide@address.com@c     <@hide@address.com@c>      Element types for search input autocomplete. Default value is "@hide@address.com,constants,hide@address.com"
	@hide@address.com@c    <@hide@address.com@c>      Generate documentation for methods and properties with given access level, default "@hide@address.com,hide@address.com"
	@hide@address.com@c         <@hide@address.com@c|@hide@address.com@c>    Generate documentation for elements marked as internal and display internal documentation parts, default "@hide@address.com@c"
	@hide@address.com@c              <@hide@address.com@c|@hide@address.com@c>    Generate documentation for PHP internal classes, default "@hide@address.com@c"
	@hide@address.com@c             <@hide@address.com@c|@hide@address.com@c>    Generate tree view of classes, interfaces, traits and exceptions, default "@hide@address.com@c"
	@hide@address.com@c       <@hide@address.com@c|@hide@address.com@c>    Generate documentation for deprecated elements, default "@hide@address.com@c"
	@hide@address.com@c             <@hide@address.com@c|@hide@address.com@c>    Generate documentation of tasks, default "@hide@address.com@c"
	@hide@address.com@c      <@hide@address.com@c|@hide@address.com@c>    Generate highlighted source code files, default "@hide@address.com@c"
	@hide@address.com@c         <@hide@address.com@c|@hide@address.com@c>    Add a link to download documentation as a ZIP archive, default "@hide@address.com@c"
	@hide@address.com@c           <@hide@address.com@c>      Save a checkstyle report of poorly documented elements into a file
	@hide@address.com@c          <@hide@address.com@c|@hide@address.com@c>    Wipe out the destination directory first, default "@hide@address.com@c"
	@hide@address.com@c            <@hide@address.com@c|@hide@address.com@c>    Don't display scaning and generating messages, default "@hide@address.com@c"
	@hide@address.com@c      <@hide@address.com@c|@hide@address.com@c>    Display progressbars, default "@hide@address.com@c"
	@hide@address.com@c           <@hide@address.com@c|@hide@address.com@c>    Use colors, default "@hide@address.com@c" on Windows, "@hide@address.com@c" on other systems
	@hide@address.com@c     <@hide@address.com@c|@hide@address.com@c>    Check for update, default "@hide@address.com@c"
	@hide@address.com@c            <@hide@address.com@c|@hide@address.com@c>    Display additional information in case of an error, default "@hide@address.com@c"
	@hide@address.com@c|@hide@address.com@c                      Display this help

Only source and destination directories are required - either set explicitly or using a config file. Configuration parameters passed via command line have precedence over parameters from a config file.

Boolean options (those with possible values @hide@address.com@c|@hide@address.com@c) do not have to have their values defined explicitly. Using @hide@address.com@c and @hide@address.com@c=@hide@address.com@c is exactly the same.

Some options can have multiple values. You can do so either by using them multiple times or by separating values by a comma. That means that writing @hide@address.com@c=@hide@address.com@c @hide@address.com@c=@hide@address.com@c or @hide@address.com@c=@hide@address.com,hide@address.com is exactly the same.

Files or directories specified by @hide@address.com@c will not be processed at all.
Elements from files within @hide@address.com@c or with @hide@address.com@c will be parsed but will not have their documentation generated. However if classes have any child classes, the full class tree will be generated and their inherited methods, properties and constants will be displayed (but will not be clickable).

HELP;
	}

	/**
	 * Checks if ApiGen is installed by PEAR.
	 *
	 * @return boolean
	 */
	public static function isInstalledByPear()
	{
		return false === strpos('@data_dir@', '@data_dir');
	}

	/**
	 * Checks if ApiGen is installed from downloaded archive.
	 *
	 * @return boolean
	 */
	public static function isInstalledByDownload()
	{
		return !self::isInstalledByPear();
	}
}
Return current item: ApiGen