Merge pull request #53 from mvriel/feature/refactor-to-v2

Make Context leaner and enable third parties to create them
This commit is contained in:
Mike van Riel
2015-06-06 21:40:28 +02:00
5 changed files with 404 additions and 154 deletions
+75
View File
@@ -0,0 +1,75 @@
<?php
/**
* This file is part of phpDocumentor.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*
* @copyright 2010-2015 Mike van Riel<[email protected]>
* @license http://www.opensource.org/licenses/mit-license.php MIT
* @link http://phpdoc.org
*/
namespace phpDocumentor\Reflection\DocBlock;
/**
* Provides information about the Context in which the DocBlock occurs that receives this context.
*
* A DocBlock does not know of its own accord in which namespace it occurs and which namespace aliases are applicable
* for the block of code in which it is in. This information is however necessary to resolve Class names in tags since
* you can provide a short form or make use of namespace aliases.
*
* The phpDocumentor Reflection component knows how to create this class but if you use the DocBlock parser from your
* own application it is possible to generate a Context class using the ContextFactory; this will analyze the file in
* which an associated class resides for its namespace and imports.
*
* @see ContextFactory::createFromClassReflector()
* @see ContextFactory::createForNamespace()
*/
final class Context
{
/** @var string The current namespace. */
private $namespace = '';
/** @var array List of namespace aliases => Fully Qualified Namespace. */
private $namespaceAliases = [];
/**
* Initializes the new context and normalizes all passed namespaces to be in Qualified Namespace Name (QNN)
* format (without a preceding `\`).
*
* @param string $namespace The namespace where this DocBlock resides in.
* @param array $namespaceAliases List of namespace aliases => Fully Qualified Namespace.
*/
public function __construct($namespace, array $namespaceAliases = [])
{
$this->namespace = ('global' !== $namespace && 'default' !== $namespace)
? trim((string)$namespace, '\\')
: '';
foreach ($namespaceAliases as $alias => $fqnn) {
$this->namespaceAliases[$alias] = trim((string)$fqnn, '\\');
}
}
/**
* Returns the Qualified Namespace Name (thus without `\` in front) where the associated element is in.
*
* @return string
*/
public function getNamespace()
{
return $this->namespace;
}
/**
* Returns a list of Qualified Namespace Names (thus without `\` in front) that are imported, the keys represent
* the alias for the imported Namespace.
*
* @return string[]
*/
public function getNamespaceAliases()
{
return $this->namespaceAliases;
}
}
+174
View File
@@ -0,0 +1,174 @@
<?php
/**
* This file is part of phpDocumentor.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*
* @copyright 2010-2015 Mike van Riel<[email protected]>
* @license http://www.opensource.org/licenses/mit-license.php MIT
* @link http://phpdoc.org
*/
namespace phpDocumentor\Reflection\DocBlock;
/**
* Convenience class to create a Context for DocBlocks when not using the Reflection Component of phpDocumentor.
*
* For a DocBlock to be able to resolve types that use partial namespace names or rely on namespace imports we need to
* provide a bit of context so that the DocBlock can read that and based on it decide how to resolve the types to
* Fully Qualified names.
*
* @see Context for more information.
*/
final class ContextFactory
{
/** The literal used at the end of a use statement. */
const T_LITERAL_END_OF_USE = ';';
/** The literal used between sets of use statements */
const T_LITERAL_USE_SEPARATOR = ',';
/**
* Build a Context given a Class Reflection.
*
* @param \ReflectionClass $class
*
* @see Context for more information on Contexts.
*
* @return Context
*/
public function createFromClassReflector(\ReflectionClass $class)
{
return $this->createForNamespace(
$class->getNamespaceName(),
file_get_contents($class->getFileName())
);
}
/**
* Build a Context for a namespace in the provided file contents.
*
* @param string $namespace It does not matter if a `\` precedes the namespace name, this method first normalizes.
* @param string $fileContents the file's contents to retrieve the aliases from with the given namespace.
*
* @see Context for more information on Contexts.
*
* @return Context
*/
public function createForNamespace($namespace, $fileContents)
{
$namespace = trim($namespace, '\\');
$useStatements = [];
$currentNamespace = '';
$tokens = new \ArrayIterator(token_get_all($fileContents));
while ($tokens->valid()) {
switch ($tokens->current()[0]) {
case T_NAMESPACE:
$currentNamespace = $this->parseNamespace($tokens);
break;
case T_USE:
if ($currentNamespace === $namespace) {
$useStatements = array_merge($useStatements, $this->parseUseStatement($tokens));
}
break;
}
$tokens->next();
}
return new Context($namespace, $useStatements);
}
/**
* Deduce the name from tokens when we are at the T_NAMESPACE token.
*
* @param \ArrayIterator $tokens
*
* @return string
*/
private function parseNamespace(\ArrayIterator $tokens)
{
// skip to the first string or namespace separator
$this->skipToNextStringOrNamespaceSeparator($tokens);
$name = '';
while ($tokens->valid() && ($tokens->current()[0] === T_STRING || $tokens->current()[0] === T_NS_SEPARATOR)
) {
$name .= $tokens->current()[1];
$tokens->next();
}
return $name;
}
/**
* Deduce the names of all imports when we are at the T_USE token.
*
* @param \ArrayIterator $tokens
*
* @return string[]
*/
private function parseUseStatement(\ArrayIterator $tokens)
{
$uses = [];
$continue = true;
while ($continue) {
$this->skipToNextStringOrNamespaceSeparator($tokens);
list($alias, $fqnn) = $this->extractUseStatement($tokens);
$uses[$alias] = $fqnn;
if ($tokens->current()[0] === self::T_LITERAL_END_OF_USE) {
$continue = false;
}
}
return $uses;
}
/**
* Fast-forwards the iterator as longs as we don't encounter a T_STRING or T_NS_SEPARATOR token.
*
* @param \ArrayIterator $tokens
*
* @return void
*/
private function skipToNextStringOrNamespaceSeparator(\ArrayIterator $tokens)
{
while ($tokens->valid() && ($tokens->current()[0] !== T_STRING) && ($tokens->current()[0] !== T_NS_SEPARATOR)) {
$tokens->next();
}
}
/**
* Deduce the namespace name and alias of an import when we are at the T_USE token or have not reached the end of
* a USE statement yet.
*
* @param \ArrayIterator $tokens
*
* @return string
*/
private function extractUseStatement(\ArrayIterator $tokens)
{
$result = [''];
while ($tokens->valid()
&& ($tokens->current()[0] !== self::T_LITERAL_USE_SEPARATOR)
&& ($tokens->current()[0] !== self::T_LITERAL_END_OF_USE)
) {
if ($tokens->current()[0] === T_AS) {
$result[] = '';
}
if ($tokens->current()[0] === T_STRING || $tokens->current()[0] === T_NS_SEPARATOR) {
$result[count($result) - 1] .= $tokens->current()[1];
}
$tokens->next();
}
if (count($result) == 1) {
$result[] = substr($result[0], strrpos($result[0], '\\') + 1);
}
return array_reverse($result);
}
}
@@ -1,154 +0,0 @@
<?php
/**
* phpDocumentor
*
* PHP Version 5.3
*
* @author Vasil Rangelov <[email protected]>
* @copyright 2010-2011 Mike van Riel / Naenius (http://www.naenius.com)
* @license http://www.opensource.org/licenses/mit-license.php MIT
* @link http://phpdoc.org
*/
namespace phpDocumentor\Reflection\DocBlock;
/**
* The context in which a DocBlock occurs.
*
* @author Vasil Rangelov <[email protected]>
* @license http://www.opensource.org/licenses/mit-license.php MIT
* @link http://phpdoc.org
*/
class Context
{
/** @var string The current namespace. */
protected $namespace = '';
/** @var array List of namespace aliases => Fully Qualified Namespace. */
protected $namespace_aliases = array();
/** @var string Name of the structural element, within the namespace. */
protected $lsen = '';
/**
* Cteates a new context.
* @param string $namespace The namespace where this DocBlock
* resides in.
* @param array $namespace_aliases List of namespace aliases => Fully
* Qualified Namespace.
* @param string $lsen Name of the structural element, within
* the namespace.
*/
public function __construct(
$namespace = '',
array $namespace_aliases = array(),
$lsen = ''
) {
if (!empty($namespace)) {
$this->setNamespace($namespace);
}
$this->setNamespaceAliases($namespace_aliases);
$this->setLSEN($lsen);
}
/**
* @return string The namespace where this DocBlock resides in.
*/
public function getNamespace()
{
return $this->namespace;
}
/**
* @return array List of namespace aliases => Fully Qualified Namespace.
*/
public function getNamespaceAliases()
{
return $this->namespace_aliases;
}
/**
* Returns the Local Structural Element Name.
*
* @return string Name of the structural element, within the namespace.
*/
public function getLSEN()
{
return $this->lsen;
}
/**
* Sets a new namespace.
*
* Sets a new namespace for the context. Leading and trailing slashes are
* trimmed, and the keywords "global" and "default" are treated as aliases
* to no namespace.
*
* @param string $namespace The new namespace to set.
*
* @return $this
*/
public function setNamespace($namespace)
{
if ('global' !== $namespace
&& 'default' !== $namespace
) {
// Srip leading and trailing slash
$this->namespace = trim((string)$namespace, '\\');
} else {
$this->namespace = '';
}
return $this;
}
/**
* Sets the namespace aliases, replacing all previous ones.
*
* @param array $namespace_aliases List of namespace aliases => Fully
* Qualified Namespace.
*
* @return $this
*/
public function setNamespaceAliases(array $namespace_aliases)
{
$this->namespace_aliases = array();
foreach ($namespace_aliases as $alias => $fqnn) {
$this->setNamespaceAlias($alias, $fqnn);
}
return $this;
}
/**
* Adds a namespace alias to the context.
*
* @param string $alias The alias name (the part after "as", or the last
* part of the Fully Qualified Namespace Name) to add.
* @param string $fqnn The Fully Qualified Namespace Name for this alias.
* Any form of leading/trailing slashes are accepted, but what will be
* stored is a name, prefixed with a slash, and no trailing slash.
*
* @return $this
*/
public function setNamespaceAlias($alias, $fqnn)
{
$this->namespace_aliases[$alias] = '\\' . trim((string)$fqnn, '\\');
return $this;
}
/**
* Sets a new Local Structural Element Name.
*
* Sets a new Local Structural Element Name. A local name also contains
* punctuation determining the kind of structural element (e.g. trailing "("
* and ")" for functions and methods).
*
* @param string $lsen The new local name of a structural element.
*
* @return $this
*/
public function setLSEN($lsen)
{
$this->lsen = (string)$lsen;
return $this;
}
}
@@ -0,0 +1,94 @@
<?php
/**
* This file is part of phpDocumentor.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*
* @copyright 2010-2015 Mike van Riel<[email protected]>
* @license http://www.opensource.org/licenses/mit-license.php MIT
* @link http://phpdoc.org
*/
namespace phpDocumentor\Reflection\DocBlock {
// Added imports on purpose as mock for the unit tests, please do not remove.
use Mockery as m;
use phpDocumentor\Reflection\DocBlock,
phpDocumentor\Reflection\DocBlock\Tag;
use \ReflectionClass; // yes, the slash is part of the test
/**
* @coversDefaultClass \phpDocumentor\Reflection\DocBlock\ContextFactory
* @covers ::<private>
*/
class ContextFactoryTest extends \PHPUnit_Framework_TestCase
{
/**
* @covers ::createFromClassReflector
* @covers ::createForNamespace
* @uses phpDocumentor\Reflection\DocBlock\Context
*/
public function testReadsNamespaceFromClassReflection()
{
$fixture = new ContextFactory();
$context = $fixture->createFromClassReflector(new ReflectionClass($this));
$this->assertSame(__NAMESPACE__, $context->getNamespace());
}
/**
* @covers ::createFromClassReflector
* @covers ::createForNamespace
* @uses phpDocumentor\Reflection\DocBlock\Context
*/
public function testReadsAliasesFromClassReflection()
{
$fixture = new ContextFactory();
$expected = [
'm' => 'Mockery',
'DocBlock' => 'phpDocumentor\Reflection\DocBlock',
'Tag' => 'phpDocumentor\Reflection\DocBlock\Tag',
'ReflectionClass' => 'ReflectionClass'
];
$context = $fixture->createFromClassReflector(new ReflectionClass($this));
$this->assertSame($expected, $context->getNamespaceAliases());
}
/**
* @covers ::createForNamespace
* @uses phpDocumentor\Reflection\DocBlock\Context
*/
public function testReadsNamespaceFromProvidedNamespaceAndContent()
{
$fixture = new ContextFactory();
$context = $fixture->createForNamespace(__NAMESPACE__, file_get_contents(__FILE__));
$this->assertSame(__NAMESPACE__, $context->getNamespace());
}
/**
* @covers ::createForNamespace
* @uses phpDocumentor\Reflection\DocBlock\Context
*/
public function testReadsAliasesFromProvidedNamespaceAndContent()
{
$fixture = new ContextFactory();
$expected = [
'm' => 'Mockery',
'DocBlock' => 'phpDocumentor\Reflection\DocBlock',
'Tag' => 'phpDocumentor\Reflection\DocBlock\Tag',
'ReflectionClass' => 'ReflectionClass'
];
$context = $fixture->createForNamespace(__NAMESPACE__, file_get_contents(__FILE__));
$this->assertSame($expected, $context->getNamespaceAliases());
}
}
}
namespace phpDocumentor\Reflection\DocBlock\Mock {
// the following import should not show in the tests above
use phpDocumentor\Reflection\DocBlock\Description;
}
@@ -0,0 +1,61 @@
<?php
/**
* This file is part of phpDocumentor.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*
* @copyright 2010-2015 Mike van Riel<[email protected]>
* @license http://www.opensource.org/licenses/mit-license.php MIT
* @link http://phpdoc.org
*/
namespace phpDocumentor\Reflection\DocBlock;
use Mockery as m;
/**
* @coversDefaultClass \phpDocumentor\Reflection\DocBlock\Context
*/
class ContextTest extends \PHPUnit_Framework_TestCase
{
/**
* @covers ::__construct
* @covers ::getNamespace
*/
public function testProvidesANormalizedNamespace()
{
$fixture = new Context('\My\Space');
$this->assertSame('My\Space', $fixture->getNamespace());
}
/**
* @covers ::__construct
* @covers ::getNamespace
*/
public function testInterpretsNamespaceNamedGlobalAsRootNamespace()
{
$fixture = new Context('global');
$this->assertSame('', $fixture->getNamespace());
}
/**
* @covers ::__construct
* @covers ::getNamespace
*/
public function testInterpretsNamespaceNamedDefaultAsRootNamespace()
{
$fixture = new Context('default');
$this->assertSame('', $fixture->getNamespace());
}
/**
* @covers ::__construct
* @covers ::getNamespaceAliases
*/
public function testProvidesNormalizedNamespaceAliases()
{
$fixture = new Context('', ['Space' => '\My\Space']);
$this->assertSame(['Space' => 'My\Space'], $fixture->getNamespaceAliases());
}
}