phpDocumentor
PHP Version 5.3
Twig | A specialized writer which uses the Twig templating engine to convert templates to HTML output. |
<?php
/**
* phpDocumentor
*
* PHP Version 5.3
*
* @copyright 2010-2014 Mike van Riel / Naenius (http://www.naenius.com)
* @license http://www.opensource.org/licenses/mit-license.php MIT
* @link http://phpdoc.org
*/
namespace phpDocumentor\Plugin\Twig\Writer;
use phpDocumentor\Descriptor\DescriptorAbstract;
use phpDocumentor\Descriptor\ProjectDescriptor;
use phpDocumentor\Plugin\Core\Transformer\Writer\Pathfinder;
use phpDocumentor\Plugin\Twig\Extension;
use phpDocumentor\Transformer\Router\ForFileProxy;
use phpDocumentor\Transformer\Router\Queue;
use phpDocumentor\Transformer\Template;
use phpDocumentor\Transformer\Transformation;
use phpDocumentor\Transformer\Writer\Routable;
use phpDocumentor\Transformer\Writer\WriterAbstract;
use phpDocumentor\Translator\Translator;
/**
* A specialized writer which uses the Twig templating engine to convert
* templates to HTML output.
*
* This writer support the Query attribute of a Transformation to generate
* multiple templates in one transformation.
*
* The Query attribute supports a simplified version of Twig queries and will
* use each individual result as the 'node' global variable in the Twig template.
*
* Example:
*
* Suppose a Query `indexes.classes` is given then this writer will be
* invoked as many times as there are classes in the project and the
* 'node' global variable in twig will be filled with each individual
* class entry.
*
* When using the Query attribute in the transformation it is important to
* use a variable in the Artifact attribute as well (otherwise the same file
* would be overwritten several times).
*
* A simple example transformation line could be:
*
* ```
* <transformation
* writer="twig"
* source="templates/twig/index.twig"
* artifact="index.html"/>
* ```
*
* This example transformation would use this writer to transform the
* index.twig template file in the twig template folder into index.html at
* the destination location.
* Since no Query is provided the 'node' global variable will contain
* the Project Descriptor of the Object Graph.
*
* A complex example transformation line could be:
*
* ```
* <transformation
* query="indexes.classes"
* writer="twig"
* source="templates/twig/class.twig"
* artifact="{{name}}.html"/>
* ```
*
* This example transformation would use this writer to transform the
* class.twig template file in the twig template folder into a file with
* the 'name' property for an individual class inside the Object Graph.
* Since a Query *is* provided will the 'node' global variable contain a
* specific instance of a class applicable to the current iteration.
*
* @see self::getDestinationPath() for more information about variables in the
* Artifact attribute.
*/
class Twig extends WriterAbstract implements Routable
{
/** @var Queue $routers */
protected $routers;
/** @var Translator $translator */
protected $translator;
/**
* This method combines the ProjectDescriptor and the given target template
* and creates a static html page at the artifact location.
*
* @param ProjectDescriptor $project Document containing the structure.
* @param Transformation $transformation Transformation to execute.
*
* @return void
*/
public function transform(ProjectDescriptor $project, Transformation $transformation)
{
$template_path = $this->getTemplatePath($transformation);
$finder = new Pathfinder();
$nodes = $finder->find($project, $transformation->getQuery());
foreach ($nodes as $node) {
if (!$node) {
continue;
}
$destination = $this->getDestinationPath($node, $transformation);
if ($destination === false) {
continue;
}
$environment = $this->initializeEnvironment($project, $transformation, $destination);
$environment->addGlobal('node', $node);
$html = $environment->render(substr($transformation->getSource(), strlen($template_path)));
file_put_contents($destination, $html);
}
}
/**
* Initializes the Twig environment with the template, base extension and additionally defined extensions.
*
* @param ProjectDescriptor $project
* @param Transformation $transformation
* @param string $destination
*
* @return \Twig_Environment
*/
protected function initializeEnvironment(ProjectDescriptor $project, Transformation $transformation, $destination)
{
$callingTemplatePath = $this->getTemplatePath($transformation);
$baseTemplatesPath = $transformation->getTransformer()->getTemplates()->getTemplatesPath();
$templateFolders = array(
$baseTemplatesPath . '/..' . DIRECTORY_SEPARATOR . $callingTemplatePath,
// http://twig.sensiolabs.org/doc/recipes.html#overriding-a-template-that-also-extends-itself
$baseTemplatesPath
);
// get all invoked template paths, they overrule the calling template path
/** @var Template $template */
foreach ($transformation->getTransformer()->getTemplates() as $template) {
$path = $baseTemplatesPath . DIRECTORY_SEPARATOR . $template->getName();
array_unshift($templateFolders, $path);
}
$env = new \Twig_Environment(
new \Twig_Loader_Filesystem($templateFolders),
array('cache' => sys_get_temp_dir() . '/phpdoc-twig-cache')
);
$this->addPhpDocumentorExtension($project, $transformation, $destination, $env);
$this->addExtensionsFromTemplateConfiguration($transformation, $project, $env);
return $env;
}
/**
* Adds the phpDocumentor base extension to the Twig Environment.
*
* @param ProjectDescriptor $project
* @param Transformation $transformation
* @param string $destination
* @param \Twig_Environment $twigEnvironment
*
* @return void
*/
protected function addPhpDocumentorExtension(
ProjectDescriptor $project,
Transformation $transformation,
$destination,
\Twig_Environment $twigEnvironment
) {
$base_extension = new Extension($project, $transformation);
$base_extension->setDestination(
substr($destination, strlen($transformation->getTransformer()->getTarget()) + 1)
);
$base_extension->setRouters($this->routers);
$base_extension->setTranslator($this->translator);
$twigEnvironment->addExtension($base_extension);
}
/**
* Tries to add any custom extensions that have been defined in the template or the transformation's configuration.
*
* This method will read the `twig-extension` parameter of the transformation (which inherits the template's
* parameter set) and try to add those extensions to the environment.
*
* @param Transformation $transformation
* @param ProjectDescriptor $project
* @param \Twig_Environment $twigEnvironment
*
* @throws \InvalidArgumentException if a twig-extension should be loaded but it could not be found.
*
* @return void
*/
protected function addExtensionsFromTemplateConfiguration(
Transformation $transformation,
ProjectDescriptor $project,
\Twig_Environment $twigEnvironment
) {
$isDebug = $transformation->getParameter('twig-debug')
? $transformation->getParameter('twig-debug')->getValue()
: false;
if ($isDebug == 'true') {
$twigEnvironment->enableDebug();
$twigEnvironment->enableAutoReload();
$twigEnvironment->addExtension(new \Twig_Extension_Debug());
}
/** @var Template\Parameter $extension */
foreach ($transformation->getParametersWithKey('twig-extension') as $extension) {
$extensionValue = $extension->getValue();
if (!class_exists($extensionValue)) {
throw new \InvalidArgumentException('Unknown twig extension: ' . $extensionValue);
}
// to support 'normal' Twig extensions we check the interface to determine what instantiation to do.
$implementsInterface = in_array(
'phpDocumentor\Plugin\Twig\ExtensionInterface',
class_implements($extensionValue)
);
$twigEnvironment->addExtension(
$implementsInterface ? new $extensionValue($project, $transformation) : new $extensionValue()
);
}
}
/**
* Uses the currently selected node and transformation to assemble the destination path for the file.
*
* The Twig writer accepts the use of a Query to be able to generate output for multiple objects using the same
* template.
*
* The given node is the result of such a query, or if no query given the selected element, and the transformation
* contains the destination file.
*
* Since it is important to be able to generate a unique name per element can the user provide a template variable
* in the name of the file.
* Such a template variable always resides between double braces and tries to take the node value of a given
* query string.
*
* Example:
*
* An artifact stating `classes/{{name}}.html` will try to find the
* node 'name' as a child of the given $node and use that value instead.
*
* @param DescriptorAbstract $node
* @param Transformation $transformation
*
* @throws \InvalidArgumentException if no artifact is provided and no routing rule matches.
* @throws \UnexpectedValueException if the provided node does not contain anything.
*
* @return string|false returns the destination location or false if generation should be aborted.
*/
protected function getDestinationPath($node, Transformation $transformation)
{
$writer = $this;
if (!$node) {
throw new \UnexpectedValueException(
'The transformation node in the twig writer is not expected to be false or null'
);
}
if (!$transformation->getArtifact()) {
$rule = $this->routers->match($node);
if (!$rule) {
throw new \InvalidArgumentException(
'No matching routing rule could be found for the given node, please provide an artifact location, '
. 'encountered: ' . ($node === null ? 'NULL' : get_class($node))
);
}
$rule = new ForFileProxy($rule);
$url = $rule->generate($node);
if ($url === false || $url[0] !== DIRECTORY_SEPARATOR) {
return false;
}
$path = $transformation->getTransformer()->getTarget() . str_replace('/', DIRECTORY_SEPARATOR, $url);
} else {
$path = $transformation->getTransformer()->getTarget()
. DIRECTORY_SEPARATOR . $transformation->getArtifact();
}
$finder = new Pathfinder();
$destination = preg_replace_callback(
'/{{([^}]+)}}/', // explicitly do not use the unicode modifier; this breaks windows
function ($query) use ($node, $writer, $finder) {
// strip any surrounding \ or /
$filepart = trim((string) current($finder->find($node, $query[1])), '\\/');
// make it windows proof
if (extension_loaded('iconv')) {
$filepart = iconv('UTF-8', 'ASCII//TRANSLIT', $filepart);
}
$filepart = strpos($filepart, '/') !== false
? implode('/', array_map('urlencode', explode('/', $filepart)))
: implode('\\', array_map('urlencode', explode('\\', $filepart)));
return $filepart;
},
$path
);
// replace any \ with the directory separator to be compatible with the
// current filesystem and allow the next file_exists to do its work
$destination = str_replace(array('/','\\'), DIRECTORY_SEPARATOR, $destination);
// create directory if it does not exist yet
if (!file_exists(dirname($destination))) {
mkdir(dirname($destination), 0777, true);
}
return $destination;
}
/**
* Returns the path belonging to the template.
*
* @param Transformation $transformation
*
* @return string
*/
protected function getTemplatePath($transformation)
{
$parts = preg_split('[\\\\|/]', $transformation->getSource());
return $parts[0] . DIRECTORY_SEPARATOR . $parts[1];
}
/**
* Sets the routers that can be used to determine the path of links.
*
* @param Queue $routers
*
* @return void
*/
public function setRouters(Queue $routers)
{
$this->routers = $routers;
}
/**
* @param Translator $translator
*/
public function setTranslator($translator)
{
$this->translator = $translator;
}
}