1<?php
2/**
3 * Zend Framework (http://framework.zend.com/)
4 *
5 * @link      http://github.com/zendframework/zf2 for the canonical source repository
6 * @copyright Copyright (c) 2005-2016 Zend Technologies USA Inc. (http://www.zend.com)
7 * @license   http://framework.zend.com/license/new-bsd New BSD License
8 */
9
10namespace Zend\Code\Scanner;
11
12use RecursiveDirectoryIterator;
13use RecursiveIteratorIterator;
14use Zend\Code\Exception;
15
16use function array_keys;
17use function array_merge;
18use function is_array;
19use function is_dir;
20use function is_string;
21use function pathinfo;
22use function realpath;
23use function sprintf;
24
25class DirectoryScanner implements ScannerInterface
26{
27    /**
28     * @var bool
29     */
30    protected $isScanned = false;
31
32    /**
33     * @var string[]|DirectoryScanner[]
34     */
35    protected $directories = [];
36
37    /**
38     * @var FileScanner[]
39     */
40    protected $fileScanners = [];
41
42    /**
43     * @var array
44     */
45    protected $classToFileScanner;
46
47    /**
48     * @param null|string|array $directory
49     */
50    public function __construct($directory = null)
51    {
52        if ($directory) {
53            if (is_string($directory)) {
54                $this->addDirectory($directory);
55            } elseif (is_array($directory)) {
56                foreach ($directory as $d) {
57                    $this->addDirectory($d);
58                }
59            }
60        }
61    }
62
63    /**
64     * @param  DirectoryScanner|string $directory
65     * @return void
66     * @throws Exception\InvalidArgumentException
67     */
68    public function addDirectory($directory)
69    {
70        if ($directory instanceof DirectoryScanner) {
71            $this->directories[] = $directory;
72        } elseif (is_string($directory)) {
73            $realDir = realpath($directory);
74            if (! $realDir || ! is_dir($realDir)) {
75                throw new Exception\InvalidArgumentException(sprintf(
76                    'Directory "%s" does not exist',
77                    $realDir
78                ));
79            }
80            $this->directories[] = $realDir;
81        } else {
82            throw new Exception\InvalidArgumentException(
83                'The argument provided was neither a DirectoryScanner or directory path'
84            );
85        }
86    }
87
88    /**
89     * @param  DirectoryScanner $directoryScanner
90     * @return void
91     */
92    public function addDirectoryScanner(DirectoryScanner $directoryScanner)
93    {
94        $this->addDirectory($directoryScanner);
95    }
96
97    /**
98     * @param  FileScanner $fileScanner
99     * @return void
100     */
101    public function addFileScanner(FileScanner $fileScanner)
102    {
103        $this->fileScanners[] = $fileScanner;
104    }
105
106    /**
107     * @return void
108     */
109    protected function scan()
110    {
111        if ($this->isScanned) {
112            return;
113        }
114
115        // iterate directories creating file scanners
116        foreach ($this->directories as $directory) {
117            if ($directory instanceof DirectoryScanner) {
118                $directory->scan();
119                if ($directory->fileScanners) {
120                    $this->fileScanners = array_merge($this->fileScanners, $directory->fileScanners);
121                }
122            } else {
123                $rdi = new RecursiveDirectoryIterator($directory);
124                foreach (new RecursiveIteratorIterator($rdi) as $item) {
125                    if ($item->isFile() && pathinfo($item->getRealPath(), PATHINFO_EXTENSION) == 'php') {
126                        $this->fileScanners[] = new FileScanner($item->getRealPath());
127                    }
128                }
129            }
130        }
131
132        $this->isScanned = true;
133    }
134
135    /**
136     * @todo implement method
137     */
138    public function getNamespaces()
139    {
140        // @todo
141    }
142
143    /**
144     * @param  bool $returnFileScanners
145     * @return array
146     */
147    public function getFiles($returnFileScanners = false)
148    {
149        $this->scan();
150
151        $return = [];
152        foreach ($this->fileScanners as $fileScanner) {
153            $return[] = $returnFileScanners ? $fileScanner : $fileScanner->getFile();
154        }
155
156        return $return;
157    }
158
159    /**
160     * @return array
161     */
162    public function getClassNames()
163    {
164        $this->scan();
165
166        if ($this->classToFileScanner === null) {
167            $this->createClassToFileScannerCache();
168        }
169
170        return array_keys($this->classToFileScanner);
171    }
172
173    /**
174     * @param  bool  $returnDerivedScannerClass
175     * @return array
176     */
177    public function getClasses($returnDerivedScannerClass = false)
178    {
179        $this->scan();
180
181        if ($this->classToFileScanner === null) {
182            $this->createClassToFileScannerCache();
183        }
184
185        $returnClasses = [];
186        foreach ($this->classToFileScanner as $className => $fsIndex) {
187            $classScanner = $this->fileScanners[$fsIndex]->getClass($className);
188            if ($returnDerivedScannerClass) {
189                $classScanner = new DerivedClassScanner($classScanner, $this);
190            }
191            $returnClasses[] = $classScanner;
192        }
193
194        return $returnClasses;
195    }
196
197    /**
198     * @param  string $class
199     * @return bool
200     */
201    public function hasClass($class)
202    {
203        $this->scan();
204
205        if ($this->classToFileScanner === null) {
206            $this->createClassToFileScannerCache();
207        }
208
209        return isset($this->classToFileScanner[$class]);
210    }
211
212    /**
213     * @param  string $class
214     * @param  bool $returnDerivedScannerClass
215     * @return ClassScanner|DerivedClassScanner
216     * @throws Exception\InvalidArgumentException
217     */
218    public function getClass($class, $returnDerivedScannerClass = false)
219    {
220        $this->scan();
221
222        if ($this->classToFileScanner === null) {
223            $this->createClassToFileScannerCache();
224        }
225
226        if (! isset($this->classToFileScanner[$class])) {
227            throw new Exception\InvalidArgumentException('Class not found.');
228        }
229
230        /** @var FileScanner $fs */
231        $fs          = $this->fileScanners[$this->classToFileScanner[$class]];
232        $returnClass = $fs->getClass($class);
233
234        if ($returnClass instanceof ClassScanner && $returnDerivedScannerClass) {
235            return new DerivedClassScanner($returnClass, $this);
236        }
237
238        return $returnClass;
239    }
240
241    /**
242     * Create class to file scanner cache
243     *
244     * @return void
245     */
246    protected function createClassToFileScannerCache()
247    {
248        if ($this->classToFileScanner !== null) {
249            return;
250        }
251
252        $this->classToFileScanner = [];
253
254        /** @var FileScanner $fileScanner */
255        foreach ($this->fileScanners as $fsIndex => $fileScanner) {
256            $fsClasses = $fileScanner->getClassNames();
257            foreach ($fsClasses as $fsClassName) {
258                $this->classToFileScanner[$fsClassName] = $fsIndex;
259            }
260        }
261    }
262
263    /**
264     * Export
265     *
266     * @todo implement method
267     */
268    public static function export()
269    {
270        // @todo
271    }
272
273    /**
274     * __ToString
275     *
276     * @todo implement method
277     */
278    public function __toString()
279    {
280        // @todo
281    }
282}
283