Test Failed
Pull Request — master (#191)
by Haydar
10:43 queued 02:56
created

SearchableListFactory::classReflector()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 7
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 4
CRAP Score 2

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 2
eloc 3
c 1
b 0
f 0
nc 2
nop 0
dl 0
loc 7
ccs 4
cts 4
cp 1
crap 2
rs 10
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Matchish\ScoutElasticSearch\Searchable;
6
7
use Illuminate\Support\Collection;
8
use Illuminate\Support\Str;
9
use Laravel\Scout\Searchable;
10
use PhpParser\Error;
11
use PhpParser\Node;
12
use PhpParser\Node\Stmt\Class_;
13
use PhpParser\NodeFinder;
14
use PhpParser\NodeTraverser;
15
use PhpParser\NodeVisitor\NameResolver;
16
use PhpParser\ParserFactory;
17
use Roave\BetterReflection\BetterReflection;
18
use Roave\BetterReflection\Reflector\Exception\IdentifierNotFound;
19
use Roave\BetterReflection\Reflector\Reflector;
20
use Symfony\Component\Finder\Finder;
21
22
final class SearchableListFactory
23
{
24
    /**
25
     * @var array|null
26
     */
27
    private static ?array $searchableClasses = null;
28
    /**
29
     * @var string
30
     */
31
    private string $namespace;
32
    /**
33
     * @var string
34
     */
35
    private string $appPath;
36
    /**
37
     * @var array
38
     */
39
    private array $errors = [];
40
    /**
41
     * @var Reflector|null
42
     */
43
    private ?Reflector $reflector = null;
44
45
    /**
46
     * @param  string  $namespace
47
     * @param  string  $appPath
48 13
     */
49
    public function __construct(string $namespace, string $appPath)
50 13
    {
51 13
        $this->namespace = $namespace;
52 13
        $this->appPath = $appPath;
53
    }
54
55
    /**
56
     * @return array
57
     */
58
    public function getErrors(): array
59
    {
60
        return $this->errors;
61
    }
62
63
    /**
64
     * @return Collection
65 13
     */
66
    public function make(): Collection
67 13
    {
68
        return new Collection($this->find());
69
    }
70
71
    /**
72
     * Get a list of searchable models.
73
     *
74
     * @return string[]
75 13
     */
76
    private function find(): array
77 13
    {
78
        $appNamespace = $this->namespace;
79
80 13
        return array_values(array_filter($this->getSearchableClasses(), static function (string $class) use ($appNamespace) {
81 13
            return Str::startsWith($class, $appNamespace);
82
        }));
83
    }
84
85
    /**
86
     * @return string[]
87 13
     */
88
    private function getSearchableClasses(): array
89 13
    {
90 1
        if (self::$searchableClasses === null) {
91
            self::$searchableClasses = $this->getProjectClasses()->filter(function ($class) {
92
                return $this->findSearchableTraitRecursively($class);
93 1
            })->toArray();
94 1
        }
95
96
        return self::$searchableClasses;
0 ignored issues
show
Bug Best Practice introduced by
The expression return self::searchableClasses could return the type null which is incompatible with the type-hinted return array. Consider adding an additional type-check to rule them out.
Loading history...
97 13
    }
98
99
    /**
100
     * @return Collection
101
     */
102
    private function getProjectClasses(): Collection
103 1
    {
104
        /** @var Class_[] $nodes */
105 1
        $nodes = (new NodeFinder())->find($this->getStmts(), function (Node $node) {
106
            return $node instanceof Class_;
107
        });
108 1
109 1
        return Collection::make($nodes)->map(function ($node) {
110
            return $node->namespacedName->toCodeString();
111
        });
112 1
    }
113 1
114
    /**
115
     * @return array
116
     */
117
    private function getStmts(): array
118
    {
119 1
        $parser = (new ParserFactory())->create(ParserFactory::PREFER_PHP7);
120
        $nameResolverVisitor = new NameResolver();
121 1
        $nodeTraverser = new NodeTraverser();
122 1
        $nodeTraverser->addVisitor($nameResolverVisitor);
123 1
        $stmts = [];
124 1
        foreach (Finder::create()->files()->name('*.php')->in($this->appPath) as $file) {
125 1
            try {
126 1
                $stmts[] = $parser->parse($file->getContents());
127
            } catch (Error $e) {
128 1
                $this->errors[] = $e->getMessage();
129
            }
130 1
        }
131
132
        $stmts = Collection::make($stmts)->flatten(1)->toArray();
133
134
        return $nodeTraverser->traverse($stmts);
135
    }
136
137 1
    /**
138 1
     * @param  string  $class
139
     * @return bool
140 1
     */
141
    private function findSearchableTraitRecursively(string $class): bool
142
    {
143
        try {
144
            $reflection = $this->reflector()->reflectClass($class);
145
146
            if (in_array(Searchable::class, $traits = $reflection->getTraitNames())) {
147 1
                return true;
148
            }
149
150 1
            foreach ($traits as $trait) {
151
                if ($this->findSearchableTraitRecursively($trait)) {
152 1
                    return true;
153 1
                }
154
            }
155
156 1
            return ($parent = $reflection->getParentClass()) && $this->findSearchableTraitRecursively($parent->getName());
157 1
        } catch (IdentifierNotFound $e) {
158 1
            $this->errors[] = $e->getMessage();
159
160
            return false;
161
        }
162 1
    }
163 1
164 1
    /**
165
     * @return Reflector
166
     */
167
    private function reflector(): Reflector
168 1
    {
169 1
        if (null === $this->reflector) {
170 1
            $this->reflector = (new BetterReflection())->reflector();
171
        }
172 1
173
        return $this->reflector;
174
    }
175
}
176