Completed
Push — develop ( 8eb671...133594 )
by Mike
19:30 queued 09:24
created

src/phpDocumentor/Transformer/Writer/Twig.php (1 issue)

Upgrade to new PHP Analysis Engine

These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more

1
<?php
2
declare(strict_types=1);
3
4
/**
5
 * This file is part of phpDocumentor.
6
 *
7
 * For the full copyright and license information, please view the LICENSE
8
 * file that was distributed with this source code.
9
 *
10
 * @author    Mike van Riel <[email protected]>
11
 * @copyright 2010-2018 Mike van Riel / Naenius (http://www.naenius.com)
12
 * @license   http://www.opensource.org/licenses/mit-license.php MIT
13
 * @link      http://phpdoc.org
14
 */
15
16
namespace phpDocumentor\Transformer\Writer;
17
18
use InvalidArgumentException;
19
use phpDocumentor\Descriptor\DescriptorAbstract;
20
use phpDocumentor\Descriptor\ProjectDescriptor;
21
use phpDocumentor\Transformer\Writer\Twig\Extension;
22
use phpDocumentor\Transformer\Router\ForFileProxy;
23
use phpDocumentor\Transformer\Router\Queue;
24
use phpDocumentor\Transformer\Transformation;
25
use Twig_Environment;
26
use Twig_Extension_Debug;
27
use Twig_Loader_Filesystem;
28
use UnexpectedValueException;
29
30
/**
31
 * A specialized writer which uses the Twig templating engine to convert
32
 * templates to HTML output.
33
 *
34
 * This writer support the Query attribute of a Transformation to generate
35
 * multiple templates in one transformation.
36
 *
37
 * The Query attribute supports a simplified version of Twig queries and will
38
 * use each individual result as the 'node' global variable in the Twig template.
39
 *
40
 * Example:
41
 *
42
 *   Suppose a Query `indexes.classes` is given then this writer will be
43
 *   invoked as many times as there are classes in the project and the
44
 *   'node' global variable in twig will be filled with each individual
45
 *   class entry.
46
 *
47
 * When using the Query attribute in the transformation it is important to
48
 * use a variable in the Artifact attribute as well (otherwise the same file
49
 * would be overwritten several times).
50
 *
51
 * A simple example transformation line could be:
52
 *
53
 *     ```
54
 *     <transformation
55
 *         writer="twig"
56
 *         source="templates/twig/index.twig"
57
 *         artifact="index.html"/>
58
 *     ```
59
 *
60
 *     This example transformation would use this writer to transform the
61
 *     index.twig template file in the twig template folder into index.html at
62
 *     the destination location.
63
 *     Since no Query is provided the 'node' global variable will contain
64
 *     the Project Descriptor of the Object Graph.
65
 *
66
 * A complex example transformation line could be:
67
 *
68
 *     ```
69
 *     <transformation
70
 *         query="indexes.classes"
71
 *         writer="twig"
72
 *         source="templates/twig/class.twig"
73
 *         artifact="{{name}}.html"/>
74
 *     ```
75
 *
76
 *     This example transformation would use this writer to transform the
77
 *     class.twig template file in the twig template folder into a file with
78
 *     the 'name' property for an individual class inside the Object Graph.
79
 *     Since a Query *is* provided will the 'node' global variable contain a
80
 *     specific instance of a class applicable to the current iteration.
81
 *
82
 * @see self::getDestinationPath() for more information about variables in the
83
 *     Artifact attribute.
84
 */
85
class Twig extends WriterAbstract implements Routable
86
{
87
    /** @var Queue $routers */
88
    protected $routers;
89
90
    /** @var Twig_Environment $twig */
91
    private $twig;
92
93
    public function __construct(Twig_Environment $twig)
94
    {
95
        $this->twig = $twig;
96
    }
97
98
    /**
99
     * This method combines the ProjectDescriptor and the given target template
100
     * and creates a static html page at the artifact location.
101
     *
102
     * @param ProjectDescriptor $project Document containing the structure.
103
     * @param Transformation $transformation Transformation to execute.
104
     */
105
    public function transform(ProjectDescriptor $project, Transformation $transformation): void
106
    {
107
        $template_path = $this->getTemplatePath($transformation);
108
109
        $finder = new Pathfinder();
110
        $nodes = $finder->find($project, $transformation->getQuery());
111
112
        foreach ($nodes as $node) {
113
            if (!$node) {
114
                continue;
115
            }
116
117
            $destination = $this->getDestinationPath($node, $transformation);
118
            if ($destination === false) {
119
                continue;
120
            }
121
122
            $environment = $this->initializeEnvironment($project, $transformation, $destination);
123
            $environment->addGlobal('node', $node);
124
125
            $html = $environment->render(substr($transformation->getSource(), strlen($template_path)));
126
            file_put_contents($destination, $html);
127
        }
128
    }
129
130
    /**
131
     * Initializes the Twig environment with the template, base extension and additionally defined extensions.
132
     */
133
    protected function initializeEnvironment(
134
        ProjectDescriptor $project,
135
        Transformation $transformation,
136
        string $destination
137
    ): Twig_Environment {
138
        $callingTemplatePath = $this->getTemplatePath($transformation);
139
140
        $baseTemplatesPath = $transformation->getTransformer()->getTemplates()->getTemplatesPath();
141
142
        $templateFolders = [
143
            $baseTemplatesPath . '/..' . DIRECTORY_SEPARATOR . $callingTemplatePath,
144
            // http://twig.sensiolabs.org/doc/recipes.html#overriding-a-template-that-also-extends-itself
145
            $baseTemplatesPath,
146
        ];
147
148
        // get all invoked template paths, they overrule the calling template path
149
        /** @var \phpDocumentor\Transformer\Template $template */
150
        foreach ($transformation->getTransformer()->getTemplates() as $template) {
151
            $path = $baseTemplatesPath . DIRECTORY_SEPARATOR . $template->getName();
152
            array_unshift($templateFolders, $path);
153
        }
154
155
        // Clone twig because otherwise we cannot re-set the extensions on this twig environment on every run of this
156
        // writer
157
        $env = clone $this->twig;
158
        $env->setLoader(new Twig_Loader_Filesystem($templateFolders));
159
160
        $this->addPhpDocumentorExtension($project, $transformation, $destination, $env);
161
        $this->addExtensionsFromTemplateConfiguration($transformation, $project, $env);
162
163
        return $env;
164
    }
165
166
    /**
167
     * Adds the phpDocumentor base extension to the Twig Environment.
168
     */
169
    protected function addPhpDocumentorExtension(
170
        ProjectDescriptor $project,
171
        Transformation $transformation,
172
        string $destination,
173
        Twig_Environment $twigEnvironment
174
    ): void {
175
        $base_extension = new Extension($project, $transformation);
176
        $base_extension->setDestination(
177
            substr($destination, strlen($transformation->getTransformer()->getTarget()) + 1)
178
        );
179
        $base_extension->setRouters($this->routers);
180
        $twigEnvironment->addExtension($base_extension);
181
    }
182
183
    /**
184
     * Tries to add any custom extensions that have been defined in the template or the transformation's configuration.
185
     *
186
     * This method will read the `twig-extension` parameter of the transformation (which inherits the template's
187
     * parameter set) and try to add those extensions to the environment.
188
     *
189
     * @throws InvalidArgumentException if a twig-extension should be loaded but it could not be found.
190
     */
191
    protected function addExtensionsFromTemplateConfiguration(
192
        Transformation $transformation,
193
        ProjectDescriptor $project,
194
        Twig_Environment $twigEnvironment
195
    ): void {
196
        $isDebug = $transformation->getParameter('twig-debug')
197
            ? $transformation->getParameter('twig-debug')->getValue()
198
            : false;
199
        if ($isDebug === 'true') {
200
            $twigEnvironment->enableDebug();
201
            $twigEnvironment->enableAutoReload();
202
            $twigEnvironment->addExtension(new Twig_Extension_Debug());
203
        }
204
205
        /** @var \phpDocumentor\Transformer\Template\Parameter $extension */
206
        foreach ($transformation->getParametersWithKey('twig-extension') as $extension) {
207
            $extensionValue = $extension->getValue();
208
            if (!class_exists($extensionValue)) {
209
                throw new InvalidArgumentException('Unknown twig extension: ' . $extensionValue);
210
            }
211
212
            // to support 'normal' Twig extensions we check the interface to determine what instantiation to do.
213
            $implementsInterface = in_array(
214
                'phpDocumentor\Transformer\Writer\Twig\ExtensionInterface',
215
                class_implements($extensionValue),
216
                true
217
            );
218
219
            $twigEnvironment->addExtension(
220
                $implementsInterface ? new $extensionValue($project, $transformation) : new $extensionValue()
221
            );
222
        }
223
    }
224
225
    /**
226
     * Uses the currently selected node and transformation to assemble the destination path for the file.
227
     *
228
     * The Twig writer accepts the use of a Query to be able to generate output for multiple objects using the same
229
     * template.
230
     *
231
     * The given node is the result of such a query, or if no query given the selected element, and the transformation
232
     * contains the destination file.
233
     *
234
     * Since it is important to be able to generate a unique name per element can the user provide a template variable
235
     * in the name of the file.
236
     * Such a template variable always resides between double braces and tries to take the node value of a given
237
     * query string.
238
     *
239
     * Example:
240
     *
241
     *   An artifact stating `classes/{{name}}.html` will try to find the
242
     *   node 'name' as a child of the given $node and use that value instead.
243
     *
244
     * @param DescriptorAbstract|ProjectDescriptor $node
245
     * @throws InvalidArgumentException if no artifact is provided and no routing rule matches.
246
     * @throws UnexpectedValueException if the provided node does not contain anything.
247
     * @return false|string returns the destination location or false if generation should be aborted.
248
     */
249
    protected function getDestinationPath($node, Transformation $transformation)
250
    {
251
        $writer = $this;
0 ignored issues
show
$writer is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
252
253
        if (!$node) {
254
            throw new UnexpectedValueException(
255
                'The transformation node in the twig writer is not expected to be false or null'
256
            );
257
        }
258
259
        if (!$transformation->getArtifact()) {
260
            $rule = $this->routers->match($node);
261
            if (!$rule) {
262
                throw new InvalidArgumentException(
263
                    'No matching routing rule could be found for the given node, please provide an artifact location, '
264
                    . 'encountered: ' . ($node === null ? 'NULL' : get_class($node))
265
                );
266
            }
267
268
            $rule = new ForFileProxy($rule);
269
            $url = $rule->generate($node);
270
            if ($url === false || $url[0] !== DIRECTORY_SEPARATOR) {
271
                return false;
272
            }
273
274
            $path = $transformation->getTransformer()->getTarget() . str_replace('/', DIRECTORY_SEPARATOR, $url);
275
        } else {
276
            $path = $transformation->getTransformer()->getTarget()
277
                . DIRECTORY_SEPARATOR . $transformation->getArtifact();
278
        }
279
280
        $finder = new Pathfinder();
281
        $destination = preg_replace_callback(
282
            '/{{([^}]+)}}/', // explicitly do not use the unicode modifier; this breaks windows
283
            function ($query) use ($node, $finder) {
284
                // strip any surrounding \ or /
285
                $filepart = trim((string) current($finder->find($node, $query[1])), '\\/');
286
287
                // make it windows proof
288
                if (extension_loaded('iconv')) {
289
                    $filepart = iconv('UTF-8', 'ASCII//TRANSLIT', $filepart);
290
                }
291
292
                return strpos($filepart, '/') !== false
293
                    ? implode('/', array_map('urlencode', explode('/', $filepart)))
294
                    : implode('\\', array_map('urlencode', explode('\\', $filepart)));
295
            },
296
            $path
297
        );
298
299
        // replace any \ with the directory separator to be compatible with the
300
        // current filesystem and allow the next file_exists to do its work
301
        $destination = str_replace(['/', '\\'], DIRECTORY_SEPARATOR, $destination);
302
303
        // create directory if it does not exist yet
304
        if (!file_exists(dirname($destination))) {
305
            mkdir(dirname($destination), 0777, true);
306
        }
307
308
        return $destination;
309
    }
310
311
    /**
312
     * Returns the path belonging to the template.
313
     */
314
    protected function getTemplatePath(Transformation $transformation): string
315
    {
316
        $parts = preg_split('[\\\\|/]', $transformation->getSource());
317
318
        return $parts[0] . DIRECTORY_SEPARATOR . $parts[1];
319
    }
320
321
    public function setRouters(Queue $routers): void
322
    {
323
        $this->routers = $routers;
324
    }
325
}
326