Completed
Push — develop ( 80740b...61b5c3 )
by Mike
10:20
created

Twig::getDestinationPath()   C

Complexity

Conditions 10
Paths 7

Size

Total Lines 61

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 10
nc 7
nop 2
dl 0
loc 61
rs 6.9842
c 0
b 0
f 0

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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
Unused Code introduced by
$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);
0 ignored issues
show
Bug introduced by
It seems like $node defined by parameter $node on line 249 can also be of type object<phpDocumentor\Des...ptor\ProjectDescriptor>; however, phpDocumentor\Transformer\Router\Queue::match() 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...
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
Bug introduced by
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
    public function setRouters(Queue $routers): void
322
    {
323
        $this->routers = $routers;
324
    }
325
}
326