1
<?php
2

3
declare(strict_types=1);
4

5
/**
6
 * Copyright (c) 2017-2020 Andreas Möller
7
 *
8
 * For the full copyright and license information, please view
9
 * the LICENSE.md file that was distributed with this source code.
10
 *
11
 * @see https://github.com/ergebnis/classy
12
 */
13

14
namespace Ergebnis\Classy;
15

16
final class Constructs
17
{
18
    /**
19
     * Returns an array of names of classy constructs (classes, interfaces, traits) found in source.
20
     *
21
     * @param string $source
22
     *
23
     * @throws Exception\ParseError
24
     *
25
     * @return Construct[]
26
     */
27 1
    public static function fromSource(string $source): array
28
    {
29 1
        $constructs = [];
30

31
        try {
32 1
            $sequence = \token_get_all(
33 1
                $source,
34 1
                \TOKEN_PARSE
35
            );
36 1
        } catch (\ParseError $exception) {
37 1
            throw Exception\ParseError::fromParseError($exception);
38
        }
39

40 1
        $count = \count($sequence);
41 1
        $namespacePrefix = '';
42

43 1
        for ($index = 0; $index < $count; ++$index) {
44 1
            $token = $sequence[$index];
45

46
            // collect namespace name
47 1
            if (\is_array($token) && \T_NAMESPACE === $token[0]) {
48 1
                $namespaceSegments = [];
49

50
                // collect namespace segments
51 1
                for ($index = self::significantAfter($index, $sequence, $count); $index < $count; ++$index) {
52 1
                    $token = $sequence[$index];
53

54 1
                    if (\is_array($token) && \T_STRING !== $token[0]) {
55 1
                        continue;
56
                    }
57

58 1
                    $content = self::content($token);
59

60 1
                    if (\in_array($content, ['{', ';'], true)) {
61 1
                        break;
62
                    }
63

64 1
                    $namespaceSegments[] = $content;
65
                }
66

67 1
                $namespace = \implode('\\', $namespaceSegments);
68 1
                $namespacePrefix = $namespace . '\\';
69
            }
70

71
            // skip non-classy tokens
72 1
            if (!\is_array($token) || !\in_array($token[0], [\T_CLASS, \T_INTERFACE, \T_TRAIT], true)) {
73 1
                continue;
74
            }
75

76
            // skip anonymous classes
77 1
            if (\T_CLASS === $token[0]) {
78 1
                $current = self::significantBefore($index, $sequence);
79 1
                $token = $sequence[$current];
80

81
                // if significant token before T_CLASS is T_NEW, it's an instantiation of an anonymous class
82 1
                if (\is_array($token) && \T_NEW === $token[0]) {
83 1
                    continue;
84
                }
85
            }
86

87 1
            $index = self::significantAfter($index, $sequence, $count);
88 1
            $token = $sequence[$index];
89

90 1
            $constructs[] = Construct::fromName($namespacePrefix . self::content($token));
91
        }
92

93
        \usort($constructs, static function (Construct $a, Construct $b): int {
94 1
            return \strcmp(
95 1
                $a->name(),
96 1
                $b->name()
97
            );
98 1
        });
99

100 1
        return $constructs;
101
    }
102

103
    /**
104
     * Returns an array of constructs defined in a directory.
105
     *
106
     * @param string $directory
107
     *
108
     * @throws Exception\DirectoryDoesNotExist
109
     * @throws Exception\MultipleDefinitionsFound
110
     *
111
     * @return Construct[]
112
     */
113 1
    public static function fromDirectory(string $directory): array
114
    {
115 1
        if (!\is_dir($directory)) {
116 1
            throw Exception\DirectoryDoesNotExist::fromDirectory($directory);
117
        }
118

119 1
        $iterator = new \RecursiveIteratorIterator(new \RecursiveDirectoryIterator(
120 1
            $directory,
121 1
            \RecursiveDirectoryIterator::FOLLOW_SYMLINKS
122
        ));
123

124 1
        $constructs = [];
125

126 1
        foreach ($iterator as $fileInfo) {
127
            /** @var \SplFileInfo $fileInfo */
128 1
            if (!$fileInfo->isFile()) {
129 1
                continue;
130
            }
131

132 1
            if ($fileInfo->getBasename('.php') === $fileInfo->getBasename()) {
133 1
                continue;
134
            }
135

136
            /** @var string $fileName */
137 1
            $fileName = $fileInfo->getRealPath();
138

139
            /** @var string $source */
140 1
            $source = \file_get_contents($fileName);
141

142
            try {
143 1
                $constructsFromFile = self::fromSource($source);
144 1
            } catch (Exception\ParseError $exception) {
145 1
                throw Exception\ParseError::fromFileNameAndParseError(
146 1
                    $fileName,
147
                    $exception
148
                );
149
            }
150

151 1
            if (0 === \count($constructsFromFile)) {
152 1
                continue;
153
            }
154

155 1
            foreach ($constructsFromFile as $construct) {
156 1
                $name = $construct->name();
157

158 1
                if (\array_key_exists($name, $constructs)) {
159 1
                    $construct = $constructs[$name];
160
                }
161

162 1
                $constructs[$name] = $construct->definedIn($fileName);
163
            }
164
        }
165

166
        \usort($constructs, static function (Construct $a, Construct $b): int {
167 1
            return \strcmp(
168 1
                $a->name(),
169 1
                $b->name()
170
            );
171 1
        });
172

173
        $constructsWithMultipleDefinitions = \array_filter($constructs, static function (Construct $construct): bool {
174 1
            return 1 < \count($construct->fileNames());
175 1
        });
176

177 1
        if (0 < \count($constructsWithMultipleDefinitions)) {
178 1
            throw Exception\MultipleDefinitionsFound::fromConstructs($constructsWithMultipleDefinitions);
179
        }
180

181 1
        return \array_values($constructs);
182
    }
183

184
    /**
185
     * Returns the index of the significant token after the index.
186
     *
187
     * @param int                                                 $index
188
     * @param array<int, array{0: int, 1: string, 2: int}|string> $sequence
189
     * @param int                                                 $count
190
     *
191
     * @return int
192
     */
193 1
    private static function significantAfter(int $index, array $sequence, int $count): int
194
    {
195 1
        for ($current = $index + 1; $current < $count; ++$current) {
196 1
            $token = $sequence[$current];
197

198 1
            if (\is_array($token) && \in_array($token[0], [\T_COMMENT, \T_DOC_COMMENT, \T_WHITESPACE], true)) {
199 1
                continue;
200
            }
201

202 1
            return $current;
203
        }
204

205 0
        throw Exception\ShouldNotHappen::create();
206
    }
207

208
    /**
209
     * Returns the index of the significant token after the index.
210
     *
211
     * @param int                                                 $index
212
     * @param array<int, array{0: int, 1: string, 2: int}|string> $sequence
213
     *
214
     * @return int
215
     */
216 1
    private static function significantBefore(int $index, array $sequence): int
217
    {
218 1
        for ($current = $index - 1; -1 < $current; --$current) {
219 1
            $token = $sequence[$current];
220

221 1
            if (\is_array($token) && \in_array($token[0], [\T_COMMENT, \T_DOC_COMMENT, \T_WHITESPACE], true)) {
222 1
                continue;
223
            }
224

225 1
            return $current;
226
        }
227

228 0
        throw Exception\ShouldNotHappen::create();
229
    }
230

231
    /**
232
     * Returns the string content of a token.
233
     *
234
     * @param array{0: int, 1: string, 2: int}|string $token
235
     *
236
     * @return string
237
     */
238 1
    private static function content($token): string
239
    {
240 1
        if (\is_array($token)) {
241 1
            return $token[1];
242
        }
243

244 1
        return $token;
245
    }
246
}

Read our documentation on viewing source code .

Loading