• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

daycry / class-finder / 16724626674

04 Aug 2025 01:32PM UTC coverage: 82.262% (-12.2%) from 94.493%
16724626674

push

github

web-flow
Update php.yml

320 of 389 relevant lines covered (82.26%)

3.29 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

92.0
/src/Libraries/PSR4/PSR4Namespace.php
1
<?php
2

3
namespace Daycry\ClassFinder\Libraries\PSR4;
4

5
use Daycry\ClassFinder\ClassFinder;
6

7
class PSR4Namespace
8
{
9
    /**
10
     * @var string
11
     */
12
    private $namespace;
13

14
    /**
15
     * @var list<string>
16
     */
17
    private $directories;
18

19
    /**
20
     * @var list<PSR4Namespace>
21
     */
22
    private $directSubnamespaces;
23

24
    /**
25
     * @param string       $namespace
26
     * @param list<string> $directories
27
     */
28
    public function __construct($namespace, $directories)
29
    {
30
        $this->namespace   = $namespace;
5✔
31
        $this->directories = $directories;
5✔
32
    }
33

34
    /**
35
     * @param string $namespace
36
     *
37
     * @return bool
38
     */
39
    public function knowsNamespace($namespace)
40
    {
41
        $numberOfSegments = count(explode('\\', $namespace));
3✔
42
        $matchingSegments = $this->countMatchingNamespaceSegments($namespace);
3✔
43

44
        if ($matchingSegments === 0) {
3✔
45
            // Provided namespace doesn't map to anything registered.
46
            return false;
3✔
47
        }
48
        if ($numberOfSegments <= $matchingSegments) {
3✔
49
            // This namespace is a superset of the provided namespace. Namespace is known.
50
            return true;
3✔
51
        }
52
        // This namespace is a subset of the provided namespace. We must resolve the remaining segments to a directory.
53
        $relativePath = substr($namespace, strlen($this->namespace));
1✔
54

55
        foreach ($this->directories as $directory) {
1✔
56
            $path = $this->normalizePath($directory, $relativePath);
1✔
57
            if (is_dir($path)) {
1✔
58
                return true;
1✔
59
            }
60
        }
61

62
        return false;
1✔
63
    }
64

65
    /**
66
     * Determines how many namespace segments match the internal namespace. This is useful because multiple namespaces
67
     * may technically match a registered namespace root, but one of the matches may be a better match. Namespaces that
68
     * match, but are not _the best_ match are incorrect matches. TestApp1\\ is **not** the best match when searching for
69
     * namespace TestApp1\\Multi\\Foo if TestApp1\\Multi was explicitly registered.
70
     *
71
     * PSR4Namespace $a;
72
     * $a->namespace = "TestApp1\\";
73
     * $a->countMatchingNamespaceSegments("TestApp1\\Multi") -> 1, TestApp1 matches.
74
     *
75
     * PSR4Namespace $b;
76
     * $b->namespace = "TestApp1\\Multi";
77
     * $b->countMatchingNamespaceSegments("TestApp1\\Multi") -> 2, TestApp1\\Multi matches
78
     *
79
     * PSR4Namespace $c;
80
     * $c->namespace = "HaydenPierce\\Foo\\Bar";
81
     * $c->countMatchingNamespaceSegments("TestApp1\\Multi") -> 0, No matches.
82
     *
83
     * @param string $namespace
84
     *
85
     * @return int
86
     */
87
    public function countMatchingNamespaceSegments($namespace)
88
    {
89
        $namespaceFragments          = explode('\\', $namespace);
5✔
90
        $undefinedNamespaceFragments = [];
5✔
91

92
        while ($namespaceFragments) {
5✔
93
            $possibleNamespace = implode('\\', $namespaceFragments) . '\\';
5✔
94

95
            if (str_contains($this->namespace, $possibleNamespace)) {
5✔
96
                return count(explode('\\', $possibleNamespace)) - 1;
5✔
97
            }
98

99
            array_unshift($undefinedNamespaceFragments, array_pop($namespaceFragments));
5✔
100
        }
101

102
        return 0;
5✔
103
    }
104

105
    /**
106
     * @param string $namespace
107
     *
108
     * @return bool
109
     */
110
    public function isAcceptableNamespace($namespace)
111
    {
112
        $namespaceSegments = count(explode('\\', $this->namespace)) - 1;
2✔
113
        $matchingSegments  = $this->countMatchingNamespaceSegments($namespace);
2✔
114

115
        return $namespaceSegments === $matchingSegments;
2✔
116
    }
117

118
    /**
119
     * @param string $namespace
120
     *
121
     * @return bool
122
     */
123
    public function isAcceptableNamespaceRecursiveMode($namespace)
124
    {
125
        // Remove prefix backslash (TODO: review if we do this eariler).
126
        $namespace = ltrim($namespace, '\\');
1✔
127

128
        return str_starts_with($this->namespace, $namespace);
1✔
129
    }
130

131
    /**
132
     * Used to identify subnamespaces.
133
     *
134
     * @return list<string>
135
     */
136
    public function findDirectories()
137
    {
138
        $self        = $this;
2✔
139
        $directories = array_reduce($this->directories, static function ($carry, $directory) use ($self) {
2✔
140
            $path          = $self->normalizePath($directory, '');
2✔
141
            $realDirectory = realpath($path);
2✔
142
            if ($realDirectory !== false) {
2✔
143
                return array_merge($carry, [$realDirectory]);
2✔
144
            }
145

146
            return $carry;
×
147
        }, []);
2✔
148

149
        $arraysOfClasses = array_map(static function ($directory) use ($self) {
2✔
150
            $files = scandir($directory);
2✔
151

152
            return array_map(static fn ($file) => $self->normalizePath($directory, $file), $files);
2✔
153
        }, $directories);
2✔
154

155
        $potentialDirectories = array_reduce($arraysOfClasses, static fn ($carry, $arrayOfClasses) => array_merge($carry, $arrayOfClasses), []);
2✔
156

157
        // Remove '.' and '..' directories
158
        $potentialDirectories = array_filter($potentialDirectories, static function ($potentialDirectory) {
2✔
159
            $segments    = explode('/', $potentialDirectory);
2✔
160
            $lastSegment = array_pop($segments);
2✔
161

162
            return $lastSegment !== '.' && $lastSegment !== '..';
2✔
163
        });
2✔
164

165
        $confirmedDirectories = array_filter($potentialDirectories, static fn ($potentialDirectory) => is_dir($potentialDirectory));
2✔
166

167
        return $confirmedDirectories;
2✔
168
    }
169

170
    /**
171
     * @param string $namespace
172
     * @param int    $options
173
     *
174
     * @return list<string>
175
     */
176
    public function findClasses($namespace, $options = ClassFinder::STANDARD_MODE)
177
    {
178
        $relativePath = substr($namespace, strlen($this->namespace));
2✔
179

180
        $self = $this;
2✔
181

182
        $directories = array_reduce($this->directories, static function ($carry, $directory) use ($relativePath, $self) {
2✔
183
            $path          = $self->normalizePath($directory, $relativePath);
2✔
184
            $realDirectory = realpath($path);
2✔
185

186
            if ($realDirectory !== false) {
2✔
187
                return array_merge($carry, [$realDirectory]);
2✔
188
            }
189

190
            return $carry;
×
191
        }, []);
2✔
192

193
        $arraysOfClasses = array_map(static fn ($directory) => scandir($directory), $directories);
2✔
194

195
        $potentialClassFiles = array_reduce($arraysOfClasses, static fn ($carry, $arrayOfClasses) => array_merge($carry, $arrayOfClasses), []);
2✔
196

197
        $potentialClasses = array_map(static fn ($file) => $namespace . '\\' . str_replace('.php', '', $file), $potentialClassFiles);
2✔
198

199
        if ($options & ClassFinder::RECURSIVE_MODE) {
2✔
200
            return $this->getClassesFromListRecursively($namespace, $options);
1✔
201
        }
202

203
        return array_filter($potentialClasses, static function ($potentialClass) use ($options) {
1✔
204
            if (! str_contains($potentialClass, 'Views')) {
1✔
205
                if (function_exists($potentialClass)) {
1✔
206
                    // For some reason calling class_exists() on a namespace'd function raises a Fatal Error (tested PHP 7.0.8)
207
                    // Example: DeepCopy\deep_copy
208
                    // return false;
209
                    return $options & ClassFinder::ALLOW_FUNCTIONS;
×
210
                }
211

212
                // return class_exists($potentialClass);
213
                return ($options & ClassFinder::ALLOW_CLASSES && class_exists($potentialClass))
1✔
214
                    || ($options & ClassFinder::ALLOW_INTERFACES && interface_exists($potentialClass, false))
1✔
215
                    || ($options & ClassFinder::ALLOW_TRAITS && trait_exists($potentialClass, false));
1✔
216
            }
217

218
            return false;
×
219
        });
1✔
220
    }
221

222
    /**
223
     * @param mixed $options
224
     *
225
     * @return list<string>
226
     */
227
    private function getDirectClassesOnly($options)
228
    {
229
        $self        = $this;
1✔
230
        $directories = array_reduce($this->directories, static function ($carry, $directory) use ($self) {
1✔
231
            $path          = $self->normalizePath($directory, '');
1✔
232
            $realDirectory = realpath($path);
1✔
233
            if ($realDirectory !== false) {
1✔
234
                return array_merge($carry, [$realDirectory]);
1✔
235
            }
236

237
            return $carry;
×
238
        }, []);
1✔
239

240
        $arraysOfClasses = array_map(static fn ($directory) => scandir($directory), $directories);
1✔
241

242
        $potentialClassFiles = array_reduce($arraysOfClasses, static fn ($carry, $arrayOfClasses) => array_merge($carry, $arrayOfClasses), []);
1✔
243

244
        $selfNamespace    = $this->namespace; // PHP 5.3 BC
1✔
245
        $potentialClasses = array_map(static fn ($file) => $selfNamespace . str_replace('.php', '', $file), $potentialClassFiles);
1✔
246

247
        return array_filter($potentialClasses, static function ($potentialClass) use ($options) {
1✔
248
            if (! str_contains($potentialClass, 'Views')) {
1✔
249
                if (function_exists($potentialClass)) {
1✔
250
                    // For some reason calling class_exists() on a namespace'd function raises a Fatal Error (tested PHP 7.0.8)
251
                    // Example: DeepCopy\deep_copy
252
                    // return false;
253
                    return $options & ClassFinder::ALLOW_FUNCTIONS;
×
254
                }
255

256
                // return class_exists($potentialClass);
257
                return ($options & ClassFinder::ALLOW_CLASSES && class_exists($potentialClass))
1✔
258
                    || ($options & ClassFinder::ALLOW_INTERFACES && interface_exists($potentialClass, false))
1✔
259
                    || ($options & ClassFinder::ALLOW_TRAITS && trait_exists($potentialClass, false));
1✔
260
            }
261

262
            return false;
×
263
        });
1✔
264
    }
265

266
    /**
267
     * The views folder does not contain classes and is excluded.
268
     *
269
     * @param string $namespace
270
     * @param mixed  $options
271
     *
272
     * @return list<string>
273
     */
274
    public function getClassesFromListRecursively($namespace, $options)
275
    {
276
        if (! str_contains($this->namespace, 'Views')) {
1✔
277
            $initialClasses = str_contains($this->namespace, $namespace) ? $this->getDirectClassesOnly($options) : [];
1✔
278
            $result         = array_reduce($this->getDirectSubnamespaces(), static fn ($carry, PSR4Namespace $subNamespace) => array_merge($carry, $subNamespace->getClassesFromListRecursively($namespace, $options)), $initialClasses);
1✔
279
        } else {
280
            $result = [];
×
281
        }
282

283
        return $result;
1✔
284
    }
285

286
    /**
287
     * Join an absolute path and a relative path in a platform agnostic way.
288
     *
289
     * This method is also extracted so that it can be turned into a vfs:// stream URL for unit testing.
290
     *
291
     * @param string $directory
292
     * @param string $relativePath
293
     *
294
     * @return mixed
295
     */
296
    public function normalizePath($directory, $relativePath)
297
    {
298
        return str_replace('\\', '/', $directory . '/' . $relativePath);
2✔
299
    }
300

301
    /**
302
     * @return list<PSR4Namespace>
303
     */
304
    public function getDirectSubnamespaces()
305
    {
306
        return $this->directSubnamespaces;
1✔
307
    }
308

309
    /**
310
     * @param list<PSR4Namespace> $directSubnamespaces
311
     */
312
    public function setDirectSubnamespaces($directSubnamespaces)
313
    {
314
        $this->directSubnamespaces = $directSubnamespaces;
2✔
315
    }
316

317
    /**
318
     * @return mixed
319
     */
320
    public function getNamespace()
321
    {
322
        return trim($this->namespace, '\\');
2✔
323
    }
324
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc