1<?php
2/*
3 * This file is part of the PHP_CodeCoverage package.
4 *
5 * (c) Sebastian Bergmann <sebastian@phpunit.de>
6 *
7 * For the full copyright and license information, please view the LICENSE
8 * file that was distributed with this source code.
9 */
10
11/**
12 * Factory for PHP_CodeCoverage_Report_Node_* object graphs.
13 *
14 * @since Class available since Release 1.1.0
15 */
16class PHP_CodeCoverage_Report_Factory
17{
18    /**
19     * @param  PHP_CodeCoverage                       $coverage
20     * @return PHP_CodeCoverage_Report_Node_Directory
21     */
22    public function create(PHP_CodeCoverage $coverage)
23    {
24        $files      = $coverage->getData();
25        $commonPath = $this->reducePaths($files);
26        $root       = new PHP_CodeCoverage_Report_Node_Directory(
27            $commonPath,
28            null
29        );
30
31        $this->addItems(
32            $root,
33            $this->buildDirectoryStructure($files),
34            $coverage->getTests(),
35            $coverage->getCacheTokens()
36        );
37
38        return $root;
39    }
40
41    /**
42     * @param PHP_CodeCoverage_Report_Node_Directory $root
43     * @param array                                  $items
44     * @param array                                  $tests
45     * @param bool                                   $cacheTokens
46     */
47    private function addItems(PHP_CodeCoverage_Report_Node_Directory $root, array $items, array $tests, $cacheTokens)
48    {
49        foreach ($items as $key => $value) {
50            if (substr($key, -2) == '/f') {
51                $key = substr($key, 0, -2);
52
53                if (file_exists($root->getPath() . DIRECTORY_SEPARATOR . $key)) {
54                    $root->addFile($key, $value, $tests, $cacheTokens);
55                }
56            } else {
57                $child = $root->addDirectory($key);
58                $this->addItems($child, $value, $tests, $cacheTokens);
59            }
60        }
61    }
62
63    /**
64     * Builds an array representation of the directory structure.
65     *
66     * For instance,
67     *
68     * <code>
69     * Array
70     * (
71     *     [Money.php] => Array
72     *         (
73     *             ...
74     *         )
75     *
76     *     [MoneyBag.php] => Array
77     *         (
78     *             ...
79     *         )
80     * )
81     * </code>
82     *
83     * is transformed into
84     *
85     * <code>
86     * Array
87     * (
88     *     [.] => Array
89     *         (
90     *             [Money.php] => Array
91     *                 (
92     *                     ...
93     *                 )
94     *
95     *             [MoneyBag.php] => Array
96     *                 (
97     *                     ...
98     *                 )
99     *         )
100     * )
101     * </code>
102     *
103     * @param  array $files
104     * @return array
105     */
106    private function buildDirectoryStructure($files)
107    {
108        $result = array();
109
110        foreach ($files as $path => $file) {
111            $path    = explode('/', $path);
112            $pointer = &$result;
113            $max     = count($path);
114
115            for ($i = 0; $i < $max; $i++) {
116                if ($i == ($max - 1)) {
117                    $type = '/f';
118                } else {
119                    $type = '';
120                }
121
122                $pointer = &$pointer[$path[$i] . $type];
123            }
124
125            $pointer = $file;
126        }
127
128        return $result;
129    }
130
131    /**
132     * Reduces the paths by cutting the longest common start path.
133     *
134     * For instance,
135     *
136     * <code>
137     * Array
138     * (
139     *     [/home/sb/Money/Money.php] => Array
140     *         (
141     *             ...
142     *         )
143     *
144     *     [/home/sb/Money/MoneyBag.php] => Array
145     *         (
146     *             ...
147     *         )
148     * )
149     * </code>
150     *
151     * is reduced to
152     *
153     * <code>
154     * Array
155     * (
156     *     [Money.php] => Array
157     *         (
158     *             ...
159     *         )
160     *
161     *     [MoneyBag.php] => Array
162     *         (
163     *             ...
164     *         )
165     * )
166     * </code>
167     *
168     * @param  array  $files
169     * @return string
170     */
171    private function reducePaths(&$files)
172    {
173        if (empty($files)) {
174            return '.';
175        }
176
177        $commonPath = '';
178        $paths      = array_keys($files);
179
180        if (count($files) == 1) {
181            $commonPath                 = dirname($paths[0]) . '/';
182            $files[basename($paths[0])] = $files[$paths[0]];
183
184            unset($files[$paths[0]]);
185
186            return $commonPath;
187        }
188
189        $max = count($paths);
190
191        for ($i = 0; $i < $max; $i++) {
192            // strip phar:// prefixes
193            if (strpos($paths[$i], 'phar://') === 0) {
194                $paths[$i] = substr($paths[$i], 7);
195                $paths[$i] = strtr($paths[$i], '/', DIRECTORY_SEPARATOR);
196            }
197            $paths[$i] = explode(DIRECTORY_SEPARATOR, $paths[$i]);
198
199            if (empty($paths[$i][0])) {
200                $paths[$i][0] = DIRECTORY_SEPARATOR;
201            }
202        }
203
204        $done = false;
205        $max  = count($paths);
206
207        while (!$done) {
208            for ($i = 0; $i < $max - 1; $i++) {
209                if (!isset($paths[$i][0]) ||
210                    !isset($paths[$i+1][0]) ||
211                    $paths[$i][0] != $paths[$i+1][0]) {
212                    $done = true;
213                    break;
214                }
215            }
216
217            if (!$done) {
218                $commonPath .= $paths[0][0];
219
220                if ($paths[0][0] != DIRECTORY_SEPARATOR) {
221                    $commonPath .= DIRECTORY_SEPARATOR;
222                }
223
224                for ($i = 0; $i < $max; $i++) {
225                    array_shift($paths[$i]);
226                }
227            }
228        }
229
230        $original = array_keys($files);
231        $max      = count($original);
232
233        for ($i = 0; $i < $max; $i++) {
234            $files[implode('/', $paths[$i])] = $files[$original[$i]];
235            unset($files[$original[$i]]);
236        }
237
238        ksort($files);
239
240        return substr($commonPath, 0, -1);
241    }
242}
243