Completed
Push — develop ( 722f70...af048b )
by Jaap
15:12 queued 05:04
created

Xsl   B

Complexity

Total Complexity 41

Size/Duplication

Total Lines 331
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 14

Importance

Changes 0
Metric Value
dl 0
loc 331
rs 8.2769
c 0
b 0
f 0
wmc 41
lcom 1
cbo 14

13 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 4 1
A checkRequirements() 0 9 3
A setRouters() 0 4 1
B transform() 0 63 7
A getXsltUriFromFilename() 0 12 3
B setProcessorParameters() 0 28 6
A getXslProcessor() 0 23 3
B loadAst() 0 25 4
A registerDefaultVariables() 0 13 3
A writeToFile() 0 7 2
A getAstPath() 0 4 1
A getArtifactPath() 0 6 2
B generateUrlForXmlElement() 0 22 5

How to fix   Complexity   

Complex Class

Complex classes like Xsl often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use Xsl, and based on these observations, apply Extract Interface, too.

1
<?php
2
/**
3
 * phpDocumentor
4
 *
5
 * PHP Version 5.3
6
 *
7
 * @copyright 2010-2011 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
12
namespace phpDocumentor\Plugin\Core\Transformer\Writer;
13
14
use Monolog\Logger;
15
use phpDocumentor\Application;
16
use phpDocumentor\Descriptor\ProjectDescriptor;
17
use phpDocumentor\Event\Dispatcher;
18
use phpDocumentor\Plugin\Core\Exception;
19
use phpDocumentor\Transformer\Event\PreXslWriterEvent;
20
use phpDocumentor\Transformer\Router\ForFileProxy;
21
use phpDocumentor\Transformer\Router\Queue;
22
use phpDocumentor\Transformer\Transformation;
23
use phpDocumentor\Transformer\Transformation as TransformationObject;
24
use phpDocumentor\Transformer\Writer\Exception\RequirementMissing;
25
use phpDocumentor\Transformer\Writer\Routable;
26
use phpDocumentor\Transformer\Writer\WriterAbstract;
27
28
/**
29
 * XSL transformation writer; generates static HTML out of the structure and XSL templates.
30
 */
31
class Xsl extends WriterAbstract implements Routable
0 ignored issues
show
Complexity introduced by
The class Xsl has a coupling between objects value of 20. Consider to reduce the number of dependencies under 13.
Loading history...
32
{
33
    /** @var \Monolog\Logger $logger */
34
    protected $logger;
35
36
    /** @var string[] */
37
    protected $xsl_variables = array();
38
39
    /** @var Queue */
40
    private $routers;
41
42
    /**
43
     * Initialize this writer with the logger so that it can output logs.
44
     *
45
     * @param Logger $logger
46
     */
47
    public function __construct(Logger $logger)
48
    {
49
        $this->logger = $logger;
50
    }
51
52
    /**
53
     * Checks whether XSL handling is enabled with PHP as that is not enabled by default.
54
     *
55
     * To enable XSL handling you need either the xsl extension or the xslcache extension installed.
56
     *
57
     * @throws RequirementMissing if neither xsl extensions are installed.
58
     *
59
     * @return void
60
     */
61
    public function checkRequirements()
62
    {
63
        if (!class_exists('XSLTProcessor') && (!extension_loaded('xslcache'))) {
64
            throw new RequirementMissing(
65
                'The XSL writer was unable to find your XSLTProcessor; '
66
                . 'please check if you have installed the PHP XSL extension or XSLCache extension'
67
            );
68
        }
69
    }
70
71
    /**
72
     * Sets the routers that can be used to determine the path of links.
73
     *
74
     * @param Queue $routers
75
     *
76
     * @return void
77
     */
78
    public function setRouters(Queue $routers)
79
    {
80
        $this->routers = $routers;
81
    }
82
83
    /**
84
     * This method combines the structure.xml and the given target template
85
     * and creates a static html page at the artifact location.
86
     *
87
     * @param ProjectDescriptor $project        Document containing the structure.
88
     * @param Transformation    $transformation Transformation to execute.
89
     *
90
     * @throws \RuntimeException if the structure.xml file could not be found.
91
     * @throws Exception        if the structure.xml file's documentRoot could not be read because of encoding issues
92
     *    or because it was absent.
93
     *
94
     * @return void
95
     */
96
    public function transform(ProjectDescriptor $project, Transformation $transformation)
97
    {
98
        $structure = $this->loadAst($this->getAstPath($transformation));
99
100
        $proc = $this->getXslProcessor($transformation);
101
        $proc->registerPHPFunctions();
102
        $this->registerDefaultVariables($transformation, $proc, $structure);
103
        $this->setProcessorParameters($transformation, $proc);
0 ignored issues
show
Bug introduced by
It seems like $proc defined by $this->getXslProcessor($transformation) on line 100 can also be of type object<XSLTCache>; however, phpDocumentor\Plugin\Cor...etProcessorParameters() does only seem to accept object<XSLTProcessor>, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
104
105
        $artifact = $this->getArtifactPath($transformation);
106
107
        $this->checkForSpacesInPath($artifact);
108
109
        // if a query is given, then apply a transformation to the artifact
110
        // location by replacing ($<var>} with the sluggified node-value of the
111
        // search result
112
        if ($transformation->getQuery() !== '') {
113
            $xpath = new \DOMXPath($structure);
114
115
            /** @var \DOMNodeList $qry */
116
            $qry = $xpath->query($transformation->getQuery());
117
            $count = $qry->length;
118
            foreach ($qry as $key => $element) {
119
                Dispatcher::getInstance()->dispatch(
120
                    'transformer.writer.xsl.pre',
121
                    PreXslWriterEvent::createInstance($this)->setElement($element)->setProgress(array($key+1, $count))
122
                );
123
124
                $proc->setParameter('', $element->nodeName, $element->nodeValue);
125
                $file_name = $transformation->getTransformer()->generateFilename(
126
                    $element->nodeValue
127
                );
128
129
                if (! $artifact) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $artifact of type string|null is loosely compared to false; this is ambiguous if the string can be empty. You might want to explicitly use === null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
130
                    $url = $this->generateUrlForXmlElement($project, $element);
131
                    if ($url === false || $url[0] !== DIRECTORY_SEPARATOR) {
132
                        continue;
133
                    }
134
135
                    $filename = $transformation->getTransformer()->getTarget()
136
                        . str_replace('/', DIRECTORY_SEPARATOR, $url);
137
                } else {
138
                    $filename = str_replace('{$' . $element->nodeName . '}', $file_name, $artifact);
139
                }
140
141
                $relativeFileName = substr($filename, strlen($transformation->getTransformer()->getTarget()) + 1);
142
                $proc->setParameter('', 'root', str_repeat('../', substr_count($relativeFileName, '/')));
143
144
                $this->writeToFile($filename, $proc, $structure);
145
            }
146
        } else {
147
            if (substr($transformation->getArtifact(), 0, 1) == '$') {
148
                // not a file, it must become a variable!
149
                $variable_name = substr($transformation->getArtifact(), 1);
150
                $this->xsl_variables[$variable_name] = $proc->transformToXml($structure);
151
            } else {
152
                $relativeFileName = substr($artifact, strlen($transformation->getTransformer()->getTarget()) + 1);
153
                $proc->setParameter('', 'root', str_repeat('../', substr_count($relativeFileName, '/')));
154
155
                $this->writeToFile($artifact, $proc, $structure);
156
            }
157
        }
158
    }
159
160
    /**
161
     * Takes the filename and converts it into a correct URI for XSLTProcessor.
162
     *
163
     * @param string $filename
164
     *
165
     * @return string
166
     */
167
    protected function getXsltUriFromFilename($filename)
168
    {
169
        // Windows requires an additional / after the scheme. If not provided then errno 22 (I/O Error: Invalid
170
        // Argument) will be raised. Thanks to @FnTmLV for finding the cause. See issue #284 for more information.
171
        // An exception to the above is when running from a Phar file; in this case the stream is handled as if on
172
        // linux; see issue #713 for more information on this exception.
173
        if (strtoupper(substr(PHP_OS, 0, 3)) == 'WIN' && ! \Phar::running()) {
174
            $filename = '/' . $filename;
175
        }
176
177
        return 'file://' . $filename;
178
    }
179
180
    /**
181
     * Sets the parameters of the XSLT processor.
182
     *
183
     * @param TransformationObject $transformation Transformation.
184
     * @param \XSLTProcessor       $proc           XSLTProcessor.
185
     *
186
     * @return void
187
     */
188
    public function setProcessorParameters(TransformationObject $transformation, $proc)
189
    {
190
        foreach ($this->xsl_variables as $key => $variable) {
191
            // XSL does not allow both single and double quotes in a string
192
            if ((strpos($variable, '"') !== false)
193
                && ((strpos($variable, "'") !== false))
194
            ) {
195
                $this->logger->warning(
196
                    'XSLT does not allow both double and single quotes in '
197
                    . 'a variable; transforming single quotes to a character '
198
                    . 'encoded version in variable: ' . $key
199
                );
200
                $variable = str_replace("'", "&#39;", $variable);
201
            }
202
203
            $proc->setParameter('', $key, $variable);
204
        }
205
206
        // add / overwrite the parameters with those defined in the
207
        // transformation entry
208
        $parameters = $transformation->getParameters();
209
        if (isset($parameters['variables'])) {
210
            /** @var \DOMElement $variable */
211
            foreach ($parameters['variables'] as $key => $value) {
0 ignored issues
show
Bug introduced by
The expression $parameters['variables'] of type object<phpDocumentor\Tra...mer\Template\Parameter> is not traversable.
Loading history...
212
                $proc->setParameter('', $key, $value);
213
            }
214
        }
215
    }
216
217
    /**
218
     *
219
     *
220
     * @param Transformation $transformation
221
     *
222
     * @return \XSLTCache|\XSLTProcessor
223
     */
224
    protected function getXslProcessor(Transformation $transformation)
225
    {
226
        $xslTemplatePath = $transformation->getSourceAsPath();
227
        $this->logger->debug('Loading XSL template: ' . $xslTemplatePath);
228
        if (!file_exists($xslTemplatePath)) {
229
            throw new Exception('Unable to find XSL template "' . $xslTemplatePath . '"');
230
        }
231
232
        if (extension_loaded('xslcache')) {
233
            $proc = new \XSLTCache();
234
            $proc->importStyleSheet($xslTemplatePath, true);
235
236
            return $proc;
237
        } else {
238
            $xsl = new \DOMDocument();
239
            $xsl->load($xslTemplatePath);
240
241
            $proc = new \XSLTProcessor();
242
            $proc->importStyleSheet($xsl);
243
244
            return $proc;
245
        }
246
    }
247
248
    /**
249
     * @param $structureFilename
250
     * @return \DOMDocument
251
     */
252
    private function loadAst($structureFilename)
253
    {
254
        if (!is_readable($structureFilename)) {
255
            throw new \RuntimeException(
256
                'Structure.xml file was not found in the target directory, is the XML writer missing from the '
257
                . 'template definition?'
258
            );
259
        }
260
261
        $structure = new \DOMDocument('1.0', 'utf-8');
262
        libxml_use_internal_errors(true);
263
        $structure->load($structureFilename);
264
265
        if (empty($structure->documentElement)) {
266
            $message = 'Specified DOMDocument lacks documentElement, cannot transform.';
267
            $error = libxml_get_last_error();
268
            if ($error) {
269
                $message .= PHP_EOL . 'Apparently an error occurred with reading the structure.xml file, the reported '
270
                    . 'error was "' . trim($error->message) . '" on line ' . $error->line;
271
            }
272
            throw new Exception($message);
273
        }
274
275
        return $structure;
276
    }
277
278
    /**
279
     * @param Transformation $transformation
280
     * @param $proc
281
     * @param $structure
282
     */
283
    private function registerDefaultVariables(Transformation $transformation, $proc, $structure)
284
    {
285
        $proc->setParameter('', 'title', $structure->documentElement->getAttribute('title'));
286
287
        if ($transformation->getParameter('search') !== null && $transformation->getParameter('search')->getValue()) {
288
            $proc->setParameter('', 'search_template', $transformation->getParameter('search')->getValue());
289
        } else {
290
            $proc->setParameter('', 'search_template', 'none');
291
        }
292
293
        $proc->setParameter('', 'version', Application::$VERSION);
294
        $proc->setParameter('', 'generated_datetime', date('r'));
295
    }
296
297
    /**
298
     * @param $filename
299
     * @param $proc
300
     * @param $structure
301
     */
302
    private function writeToFile($filename, $proc, $structure)
303
    {
304
        if (!file_exists(dirname($filename))) {
305
            mkdir(dirname($filename), 0755, true);
306
        }
307
        $proc->transformToURI($structure, $this->getXsltUriFromFilename($filename));
308
    }
309
310
    /**
311
     * @param Transformation $transformation
312
     * @return string
313
     */
314
    private function getAstPath(Transformation $transformation)
315
    {
316
        return $transformation->getTransformer()->getTarget() . DIRECTORY_SEPARATOR . 'structure.xml';
317
    }
318
319
    /**
320
     * Returns the path to the location where the artifact should be written, or null to automatically detect the
321
     * location using the router.
322
     *
323
     * @param Transformation $transformation
324
     *
325
     * @return string|null
326
     */
327
    private function getArtifactPath(Transformation $transformation)
328
    {
329
        return $transformation->getArtifact()
330
            ? $transformation->getTransformer()->getTarget() . DIRECTORY_SEPARATOR . $transformation->getArtifact()
331
            : null;
332
    }
333
334
    /**
335
     * @param ProjectDescriptor $project
336
     * @param $element
337
     * @return false|string
338
     */
339
    private function generateUrlForXmlElement(ProjectDescriptor $project, $element)
340
    {
341
        $elements = $project->getIndexes()->get('elements');
342
343
        $elementFqcn = ($element->parentNode->nodeName === 'namespace' ? '~\\' : '') . $element->nodeValue;
344
        $node = (isset($elements[$elementFqcn]))
345
            ? $elements[$elementFqcn]
346
            : $element->nodeValue; // do not use the normalized version if the element is not found!
347
348
        $rule = $this->routers->match($node);
349
        if (!$rule) {
350
            throw new \InvalidArgumentException(
351
                'No matching routing rule could be found for the given node, please provide an artifact location, '
352
                . 'encountered: ' . ($node === null ? 'NULL' : get_class($node))
353
            );
354
        }
355
356
        $rule = new ForFileProxy($rule);
357
        $url = $rule->generate($node);
358
359
        return $url;
360
    }
361
}
362