PageRenderTime 65ms CodeModel.GetById 31ms RepoModel.GetById 1ms app.codeStats 0ms

/src/phpDocumentor/Plugin/Core/Transformer/Writer/Twig.php

https://github.com/raulduc/phpDocumentor2
PHP | 398 lines | 200 code | 31 blank | 167 comment | 17 complexity | 323e7a10db26ec31f488cb1e078460af MD5 | raw file
Possible License(s): LGPL-3.0
  1. <?php
  2. /**
  3. * phpDocumentor
  4. *
  5. * PHP Version 5.3
  6. *
  7. * @copyright 2010-2013 Mike van Riel / Naenius (http://www.naenius.com)
  8. * @license http://www.opensource.org/licenses/mit-license.php MIT
  9. * @link http://phpdoc.org
  10. */
  11. namespace phpDocumentor\Plugin\Core\Transformer\Writer;
  12. use phpDocumentor\Descriptor\DescriptorAbstract;
  13. use phpDocumentor\Descriptor\ProjectDescriptor;
  14. use phpDocumentor\Plugin\Core\Twig\Extension;
  15. use phpDocumentor\Transformer\Router\Queue;
  16. use phpDocumentor\Transformer\Template;
  17. use phpDocumentor\Transformer\Transformation;
  18. use phpDocumentor\Transformer\Writer\Routable;
  19. use phpDocumentor\Transformer\Writer\WriterAbstract;
  20. use phpDocumentor\Translator;
  21. /**
  22. * A specialized writer which uses the Twig templating engine to convert
  23. * templates to HTML output.
  24. *
  25. * This writer support the Query attribute of a Transformation to generate
  26. * multiple templates in one transformation.
  27. *
  28. * The Query attribute supports a simplified version of Twig queries and will
  29. * use each individual result as the 'node' global variable in the Twig template.
  30. *
  31. * Example:
  32. *
  33. * Suppose a Query `indexes.classes` is given then this writer will be
  34. * invoked as many times as there are classes in the project and the
  35. * 'node' global variable in twig will be filled with each individual
  36. * class entry.
  37. *
  38. * When using the Query attribute in the transformation it is important to
  39. * use a variable in the Artefact attribute as well (otherwise the same file
  40. * would be overwritten several times).
  41. *
  42. * A simple example transformation line could be:
  43. *
  44. * ```
  45. * <transformation
  46. * writer="twig"
  47. * source="templates/twig/index.twig"
  48. * artifact="index.html"/>
  49. * ```
  50. *
  51. * This example transformation would use this writer to transform the
  52. * index.twig template file in the twig template folder into index.html at
  53. * the destination location.
  54. * Since no Query is provided the 'node' global variable will contain
  55. * the Project Descriptor of the Object Graph.
  56. *
  57. * A complex example transformation line could be:
  58. *
  59. * ```
  60. * <transformation
  61. * query="indexes.classes"
  62. * writer="twig"
  63. * source="templates/twig/class.twig"
  64. * artifact="{{name}}.html"/>
  65. * ```
  66. *
  67. * This example transformation would use this writer to transform the
  68. * class.twig template file in the twig template folder into a file with
  69. * the 'name' poperty for an individual class inside the Object Graph.
  70. * Since a Query *is* provided will the 'node' global variable contain a
  71. * specific instance of a class applicable to the current iteration.
  72. *
  73. * @see self::getDestinationPath() for more information about variables in the
  74. * Artefact attribute.
  75. */
  76. class Twig extends WriterAbstract implements Routable
  77. {
  78. /** @var Queue $routers */
  79. protected $routers;
  80. /** @var Translator $translator */
  81. protected $translator;
  82. /**
  83. * This method combines the ProjectDescriptor and the given target template
  84. * and creates a static html page at the artifact location.
  85. *
  86. * @param ProjectDescriptor $project Document containing the structure.
  87. * @param Transformation $transformation Transformation to execute.
  88. *
  89. * @return void
  90. */
  91. public function transform(ProjectDescriptor $project, Transformation $transformation)
  92. {
  93. $template_path = $this->getTemplatePath($transformation);
  94. $nodes = $this->getListOfNodes($transformation->getQuery(), $project);
  95. foreach ($nodes as $node) {
  96. if (!$node) {
  97. continue;
  98. }
  99. $destination = $this->getDestinationPath($node, $transformation);
  100. if ($destination === false) {
  101. continue;
  102. }
  103. $environment = $this->initializeEnvironment($project, $transformation, $destination);
  104. $environment->addGlobal('node', $node);
  105. $html = $environment->render(substr($transformation->getSource(), strlen($template_path)));
  106. file_put_contents($destination, $html);
  107. }
  108. }
  109. /**
  110. * Combines the query and project to retrieve a list of nodes that are to be used as node-point in a template.
  111. *
  112. * This method interprets the provided query string and walks through the project descriptor to find the correct
  113. * element. This method will silently fail if an invalid query was provided; in such a case the project descriptor
  114. * is returned.
  115. *
  116. * @param string $query
  117. * @param ProjectDescriptor $project
  118. *
  119. * @return \Traversable|mixed[]
  120. */
  121. protected function getListOfNodes($query, ProjectDescriptor $project)
  122. {
  123. if ($query) {
  124. $node = $this->walkObjectTree($project, $query);
  125. if (!is_array($node) && (!$node instanceof \Traversable)) {
  126. $node = array($node);
  127. }
  128. return $node;
  129. }
  130. return array($project);
  131. }
  132. /**
  133. * Walks an object graph and/or array using a twig query string.
  134. *
  135. * Note: this method is public because it is used in a closure in {{@see getDestinationPath()}}.
  136. *
  137. * @param \Traversable|mixed $objectOrArray
  138. * @param string $query A path to walk separated by dots, i.e. `namespace.namespaces`.
  139. *
  140. * @todo move this to a separate class and make it more flexible.
  141. *
  142. * @return mixed
  143. */
  144. public function walkObjectTree($objectOrArray, $query)
  145. {
  146. $node = $objectOrArray;
  147. $objectPath = explode('.', $query);
  148. // walk through the tree
  149. foreach ($objectPath as $pathNode) {
  150. if (is_array($node)) {
  151. if (isset($node[$pathNode])) {
  152. $node = $node[$pathNode];
  153. continue;
  154. }
  155. } elseif (is_object($node)) {
  156. if (isset($node->$pathNode) || (method_exists($node, '__get') && $node->$pathNode)) {
  157. $node = $node->$pathNode;
  158. continue;
  159. } elseif (method_exists($node, $pathNode)) {
  160. $node = $node->$pathNode();
  161. continue;
  162. } elseif (method_exists($node, 'get' . $pathNode)) {
  163. $pathNode = 'get' . $pathNode;
  164. $node = $node->$pathNode();
  165. continue;
  166. } elseif (method_exists($node, 'is' . $pathNode)) {
  167. $pathNode = 'is' . $pathNode;
  168. $node = $node->$pathNode();
  169. continue;
  170. }
  171. }
  172. return null;
  173. }
  174. return $node;
  175. }
  176. /**
  177. * Initializes the Twig environment with the template, base extension and additionally defined extensions.
  178. *
  179. * @param ProjectDescriptor $project
  180. * @param Transformation $transformation
  181. * @param string $destination
  182. *
  183. * @return \Twig_Environment
  184. */
  185. protected function initializeEnvironment(ProjectDescriptor $project, Transformation $transformation, $destination)
  186. {
  187. $callingTemplatePath = $this->getTemplatePath($transformation);
  188. $baseTemplatesPath = $transformation->getTransformer()->getTemplates()->getTemplatesPath();
  189. $templateFolders = array(
  190. $baseTemplatesPath . '/..' . DIRECTORY_SEPARATOR . $callingTemplatePath,
  191. // http://twig.sensiolabs.org/doc/recipes.html#overriding-a-template-that-also-extends-itself
  192. $baseTemplatesPath
  193. );
  194. // get all invoked template paths, they overrule the calling template path
  195. /** @var Template $template */
  196. foreach ($transformation->getTransformer()->getTemplates() as $template) {
  197. $path = $baseTemplatesPath . DIRECTORY_SEPARATOR . $template->getName();
  198. array_unshift($templateFolders, $path);
  199. }
  200. $env = new \Twig_Environment(new \Twig_Loader_Filesystem($templateFolders));
  201. $this->addPhpDocumentorExtension($project, $transformation, $destination, $env);
  202. $this->addExtensionsFromTemplateConfiguration($transformation, $project, $env);
  203. return $env;
  204. }
  205. /**
  206. * Adds the phpDocumentor base extension to the Twig Environment.
  207. *
  208. * @param ProjectDescriptor $project
  209. * @param Transformation $transformation
  210. * @param string $destination
  211. * @param \Twig_Environment $twigEnvironment
  212. *
  213. * @return void
  214. */
  215. protected function addPhpDocumentorExtension(
  216. ProjectDescriptor $project,
  217. Transformation $transformation,
  218. $destination,
  219. \Twig_Environment $twigEnvironment
  220. ) {
  221. $base_extension = new Extension($project, $transformation);
  222. $base_extension->setDestination(
  223. substr($destination, strlen($transformation->getTransformer()->getTarget()) + 1)
  224. );
  225. $base_extension->setRouters($this->routers);
  226. $base_extension->setTranslator($this->translator);
  227. $twigEnvironment->addExtension($base_extension);
  228. }
  229. /**
  230. * Tries to add any custom extensions that have been defined in the template or the transformation's configuration.
  231. *
  232. * This method will read the `twig-extension` parameter of the transformation (which inherits the template's
  233. * parameter set) and try to add those extensions to the environment.
  234. *
  235. * @param Transformation $transformation
  236. * @param ProjectDescriptor $project
  237. * @param \Twig_Environment $twigEnvironment
  238. *
  239. * @throws \InvalidArgumentException if a twig-extension should be loaded but it could not be found.
  240. *
  241. * @return void
  242. */
  243. protected function addExtensionsFromTemplateConfiguration(
  244. Transformation $transformation,
  245. ProjectDescriptor $project,
  246. \Twig_Environment $twigEnvironment
  247. ) {
  248. /** @var \SimpleXMLElement $extension */
  249. foreach ((array)$transformation->getParameter('twig-extension', array()) as $extension) {
  250. $extension = (string)$extension;
  251. if (!class_exists($extension)) {
  252. throw new \InvalidArgumentException('Unknown twig extension: ' . $extension);
  253. }
  254. // to support 'normal' Twig extensions we check the interface to determine what instantiation to do.
  255. $implementsInterface = in_array(
  256. 'phpDocumentor\Plugin\Core\Twig\ExtensionInterface',
  257. class_implements($extension)
  258. );
  259. $twigEnvironment->addExtension(
  260. $implementsInterface ? new $extension($project, $transformation) : new $extension()
  261. );
  262. }
  263. }
  264. /**
  265. * Uses the currently selected node and transformation to assemble the destination path for the file.
  266. *
  267. * The Twig writer accepts the use of a Query to be able to generate output for multiple objects using the same
  268. * template.
  269. *
  270. * The given node is the result of such a query, or if no query given the selected element, and the transformation
  271. * contains the destination file.
  272. *
  273. * Since it is important to be able to generate a unique name per element can the user provide a template variable
  274. * in the name of the file.
  275. * Such a template variable always resides between double braces and tries to take the node value of a given
  276. * query string.
  277. *
  278. * Example:
  279. *
  280. * An artefact stating `classes/{{name}}.html` will try to find the
  281. * node 'name' as a child of the given $node and use that value instead.
  282. *
  283. * @param DescriptorAbstract $node
  284. * @param Transformation $transformation
  285. *
  286. * @throws \InvalidArgumentException if no artifact is provided and no routing rule matches.
  287. *
  288. * @return string|false returns the destination location or false if generation should be aborted.
  289. */
  290. protected function getDestinationPath($node, Transformation $transformation)
  291. {
  292. $writer = $this;
  293. if (!$node) {
  294. throw new \UnexpectedValueException(
  295. 'The transformation node in the twig writer is not expected to be false or null'
  296. );
  297. }
  298. if (!$transformation->getArtifact()) {
  299. $rule = $this->routers->match($node);
  300. if (!$rule) {
  301. throw new \InvalidArgumentException(
  302. 'No matching routing rule could be found for the given node, please provide an artifact location, '
  303. . 'encountered: ' . ($node === null ? 'NULL' : get_class($node))
  304. );
  305. }
  306. $url = $rule->generate($node);
  307. if ($url === false || $url[0] !== '/') {
  308. return false;
  309. }
  310. $path = $transformation->getTransformer()->getTarget() . str_replace('/', DIRECTORY_SEPARATOR, $url);
  311. } else {
  312. $path = $transformation->getTransformer()->getTarget()
  313. . DIRECTORY_SEPARATOR . $transformation->getArtifact();
  314. }
  315. $destination = preg_replace_callback(
  316. '/{{([^}]+)}}/u',
  317. function ($query) use ($node, $writer) {
  318. // strip any surrounding \ or /
  319. return trim((string)$writer->walkObjectTree($node, $query[1]), '\\/');
  320. },
  321. $path
  322. );
  323. // replace any \ with the directory separator to be compatible with the
  324. // current filesystem and allow the next file_exists to do its work
  325. $destination = str_replace('\\', DIRECTORY_SEPARATOR, $destination);
  326. // create directory if it does not exist yet
  327. if (!file_exists(dirname($destination))) {
  328. mkdir(dirname($destination), 0777, true);
  329. }
  330. return $destination;
  331. }
  332. /**
  333. * Returns the path belonging to the template.
  334. *
  335. * @param Transformation $transformation
  336. *
  337. * @return string
  338. */
  339. protected function getTemplatePath($transformation)
  340. {
  341. $parts = preg_split('[\\\\|/]', $transformation->getSource());
  342. return $parts[0] . DIRECTORY_SEPARATOR . $parts[1];
  343. }
  344. /**
  345. * Sets the routers that can be used to determine the path of links.
  346. *
  347. * @param Queue $routers
  348. *
  349. * @return void
  350. */
  351. public function setRouters(Queue $routers)
  352. {
  353. $this->routers = $routers;
  354. }
  355. /**
  356. * @param \phpDocumentor\Translator $translator
  357. */
  358. public function setTranslator($translator)
  359. {
  360. $this->translator = $translator;
  361. }
  362. }