Completed
Push — develop ( 0b8425...c51867 )
by Jaap
15s queued 11s
created

src/phpDocumentor/Plugin/Twig/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\Plugin\Twig\Writer;
17
18
use InvalidArgumentException;
19
use phpDocumentor\Descriptor\DescriptorAbstract;
20
use phpDocumentor\Descriptor\ProjectDescriptor;
21
use phpDocumentor\Plugin\Core\Transformer\Writer\Pathfinder;
22
use phpDocumentor\Plugin\Twig\Extension;
23
use phpDocumentor\Transformer\Router\ForFileProxy;
24
use phpDocumentor\Transformer\Router\Queue;
25
use phpDocumentor\Transformer\Transformation;
26
use phpDocumentor\Transformer\Writer\Routable;
27
use phpDocumentor\Transformer\Writer\WriterAbstract;
28
use Twig_Environment;
29
use Twig_Extension_Debug;
30
use Twig_Loader_Filesystem;
31
use UnexpectedValueException;
32
33
/**
34
 * A specialized writer which uses the Twig templating engine to convert
35
 * templates to HTML output.
36
 *
37
 * This writer support the Query attribute of a Transformation to generate
38
 * multiple templates in one transformation.
39
 *
40
 * The Query attribute supports a simplified version of Twig queries and will
41
 * use each individual result as the 'node' global variable in the Twig template.
42
 *
43
 * Example:
44
 *
45
 *   Suppose a Query `indexes.classes` is given then this writer will be
46
 *   invoked as many times as there are classes in the project and the
47
 *   'node' global variable in twig will be filled with each individual
48
 *   class entry.
49
 *
50
 * When using the Query attribute in the transformation it is important to
51
 * use a variable in the Artifact attribute as well (otherwise the same file
52
 * would be overwritten several times).
53
 *
54
 * A simple example transformation line could be:
55
 *
56
 *     ```
57
 *     <transformation
58
 *         writer="twig"
59
 *         source="templates/twig/index.twig"
60
 *         artifact="index.html"/>
61
 *     ```
62
 *
63
 *     This example transformation would use this writer to transform the
64
 *     index.twig template file in the twig template folder into index.html at
65
 *     the destination location.
66
 *     Since no Query is provided the 'node' global variable will contain
67
 *     the Project Descriptor of the Object Graph.
68
 *
69
 * A complex example transformation line could be:
70
 *
71
 *     ```
72
 *     <transformation
73
 *         query="indexes.classes"
74
 *         writer="twig"
75
 *         source="templates/twig/class.twig"
76
 *         artifact="{{name}}.html"/>
77
 *     ```
78
 *
79
 *     This example transformation would use this writer to transform the
80
 *     class.twig template file in the twig template folder into a file with
81
 *     the 'name' property for an individual class inside the Object Graph.
82
 *     Since a Query *is* provided will the 'node' global variable contain a
83
 *     specific instance of a class applicable to the current iteration.
84
 *
85
 * @see self::getDestinationPath() for more information about variables in the
86
 *     Artifact attribute.
87
 */
88
class Twig extends WriterAbstract implements Routable
89
{
90
    /** @var Queue $routers */
91
    protected $routers;
92
93
    /** @var Twig_Environment $twig */
94
    private $twig;
95
96 1
    public function __construct(Twig_Environment $twig)
97
    {
98 1
        $this->twig = $twig;
99 1
    }
100
101
    /**
102
     * This method combines the ProjectDescriptor and the given target template
103
     * and creates a static html page at the artifact location.
104
     *
105
     * @param ProjectDescriptor $project Document containing the structure.
106
     * @param Transformation $transformation Transformation to execute.
107
     */
108
    public function transform(ProjectDescriptor $project, Transformation $transformation): void
109
    {
110
        $template_path = $this->getTemplatePath($transformation);
111
112
        $finder = new Pathfinder();
113
        $nodes = $finder->find($project, $transformation->getQuery());
114
115
        foreach ($nodes as $node) {
116
            if (!$node) {
117
                continue;
118
            }
119
120
            $destination = $this->getDestinationPath($node, $transformation);
121
            if ($destination === false) {
122
                continue;
123
            }
124
125
            $environment = $this->initializeEnvironment($project, $transformation, $destination);
126
            $environment->addGlobal('node', $node);
127
128
            $html = $environment->render(substr($transformation->getSource(), strlen($template_path)));
129
            file_put_contents($destination, $html);
130
        }
131
    }
132
133
    /**
134
     * Initializes the Twig environment with the template, base extension and additionally defined extensions.
135
     */
136
    protected function initializeEnvironment(ProjectDescriptor $project, Transformation $transformation, string $destination): Twig_Environment
137
    {
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\Plugin\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;
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);
0 ignored issues
show
It seems like $node defined by parameter $node on line 249 can also be of type object<phpDocumentor\Des...ptor\ProjectDescriptor>; however, phpDocumentor\Transforme...orFileProxy::generate() does only seem to accept string|object<phpDocumen...tor\DescriptorAbstract>, maybe add an additional type check?

This check looks at variables that have been passed in as parameters and are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
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 1
    public function setRouters(Queue $routers): void
322
    {
323 1
        $this->routers = $routers;
324 1
    }
325
}
326