Issues (10)

src/Service/FacadeHelperGenerator.php (3 issues)

1
<?php
2
3
namespace Flawlol\FacadeIdeHelper\Service;
4
5
use Flawlol\Facade\Abstract\Facade;
6
use Flawlol\FacadeIdeHelper\Interface\FacadeHelperGeneratorInterface;
7
use ReflectionParameter;
8
use SplFileObject;
9
use Symfony\Component\DependencyInjection\ContainerInterface;
10
use Symfony\Component\Finder\Finder;
11
12
/**
13
 * Class FacadeHelperGenerator.
14
 *
15
 * This class is responsible for generating facade helper files for IDEs.
16
 *
17
 * @author Flawlol - Norbert Kecső
18
 */
19
final class FacadeHelperGenerator implements FacadeHelperGeneratorInterface
20
{
21
    /**
22
     * @var string The current namespace being processed.
23
     */
24
    private string $currentNamespace = '';
25
26
    /**
27
     * FacadeHelperGenerator constructor.
28
     *
29
     * @param ContainerInterface $container The container interface for dependency injection.
30
     */
31
    public function __construct(private ContainerInterface $container)
32
    {
33
    }
34
35
    /**
36
     * Generate the facade helper file.
37
     *
38
     * This method scans the source directory for PHP files, identifies classes that extend the Facade class,
39
     * and writes their method signatures to a helper file for IDE autocompletion.
40
     */
41
    public function generate(): void
42
    {
43
        $projectDir = $this->container->getParameter('kernel.project_dir');
0 ignored issues
show
The method getParameter() does not exist on Psr\Container\ContainerInterface. It seems like you code against a sub-type of Psr\Container\ContainerInterface such as Symfony\Component\Depend...tion\ContainerInterface. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

43
        /** @scrutinizer ignore-call */ 
44
        $projectDir = $this->container->getParameter('kernel.project_dir');
Loading history...
44
        $srcDir = $projectDir.'/src';
45
46
        $finder = new Finder();
47
        $finder->files()->in($srcDir)->name('*.php');
48
49
        $helperFile = new SplFileObject('_ide-helper.php', 'w');
50
        $helperFile->fwrite("<?php\n");
51
        $helperFile->fwrite("\n");
52
53
        foreach ($finder as $file) {
54
            $className = $this->getClassNameFromFile($file->getRealPath());
55
56
            if (!class_exists($className)) {
0 ignored issues
show
It seems like $className can also be of type null; however, parameter $class of class_exists() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

56
            if (!class_exists(/** @scrutinizer ignore-type */ $className)) {
Loading history...
57
                continue;
58
            }
59
60
            $reflectionClass = new \ReflectionClass($className);
61
62
            if ($reflectionClass->isAbstract() || $reflectionClass->isInterface()) {
63
                continue;
64
            }
65
66
            $object = $reflectionClass->newInstanceWithoutConstructor();
67
68
            if ($this->isInstanceOfWithoutInitializing($object)) {
69
                $this->writeNamespaceAndClass($helperFile, $reflectionClass, $object);
70
            }
71
        }
72
73
        if ($this->currentNamespace !== '') {
74
            $helperFile->fwrite("}\n\n");
75
        }
76
    }
77
78
    /**
79
     * Get the fully qualified class name from a file.
80
     *
81
     * @param string $filePath The path to the file.
82
     *
83
     * @return string|null The fully qualified class name, or null if not found.
84
     */
85
    private function getClassNameFromFile(string $filePath): ?string
86
    {
87
        $contents = file_get_contents($filePath);
88
        $namespace = '';
89
90
        if (preg_match('/namespace\s+(.+?);/', $contents, $matches)) {
91
            $namespace = $matches[1];
92
        }
93
94
        if (preg_match('/class\s+(\w+)/', $contents, $matches)) {
95
            return $namespace ? $namespace.'\\'.$matches[1] : $matches[1];
96
        }
97
98
        return null;
99
    }
100
101
    /**
102
     * Check if a class is an instance of the Facade class without initializing it.
103
     *
104
     * @param object|null $className The class to check.
105
     *
106
     * @return bool True if the class is an instance of the Facade class, false otherwise.
107
     */
108
    private function isInstanceOfWithoutInitializing(object $className = null): bool
109
    {
110
        return is_subclass_of($className, Facade::class) || in_array(Facade::class, class_implements($className));
111
    }
112
113
    /**
114
     * Write the namespace and class structure to the helper file.
115
     *
116
     * @param SplFileObject    $helperFile      The helper file object.
117
     * @param \ReflectionClass $reflectionClass The reflection class object.
118
     * @param object           $object          The class object.
119
     */
120
    private function writeNamespaceAndClass(SplFileObject $helperFile, \ReflectionClass $reflectionClass, $object): void
121
    {
122
        $namespace = $reflectionClass->getNamespaceName();
123
124
        if ($namespace !== $this->currentNamespace) {
125
            if ($this->currentNamespace !== '') {
126
                $helperFile->fwrite("}\n\n");
127
            }
128
129
            $this->currentNamespace = $namespace;
130
            $helperFile->fwrite("namespace {$namespace} {\n");
131
            $helperFile->fwrite("\n");
132
        }
133
134
        $helperFile->fwrite("    class {$reflectionClass->getShortName()}\n");
135
        $helperFile->fwrite("    {\n");
136
137
        $service = $object::getFacadeAccessor();
138
        $serviceInstance = $this->container->get($service);
139
        $serviceInstanceReflection = new \ReflectionClass($serviceInstance);
140
        $methods = $serviceInstanceReflection->getMethods();
141
142
        foreach ($methods as $method) {
143
            if ($method->isConstructor()) {
144
                continue;
145
            }
146
147
            if ($method->isPublic()) {
148
                $this->writeMethod($helperFile, $method, $serviceInstanceReflection);
149
            }
150
        }
151
152
        $helperFile->fwrite("    }\n");
153
    }
154
155
    /**
156
     * Write the method signature and docblock to the helper file.
157
     *
158
     * @param SplFileObject     $helperFile                The helper file object.
159
     * @param \ReflectionMethod $method                    The reflection method object.
160
     * @param \ReflectionClass  $serviceInstanceReflection The reflection class object of the service instance.
161
     */
162
    private function writeMethod(SplFileObject $helperFile, \ReflectionMethod $method, \ReflectionClass $serviceInstanceReflection): void
163
    {
164
        $params = [];
165
166
        foreach ($method->getParameters() as $param) {
167
            $paramType = $param->getType();
168
            $type = $paramType ? $paramType->getName().' ' : '';
0 ignored issues
show
The method getName() does not exist on ReflectionType. It seems like you code against a sub-type of ReflectionType such as ReflectionNamedType. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

168
            $type = $paramType ? $paramType->/** @scrutinizer ignore-call */ getName().' ' : '';
Loading history...
169
            $defaultValue = $param->isOptional() ? ' = '.var_export($param->getDefaultValue(), true) : '';
170
            $params[] = $type.'$'.$param->getName().$defaultValue;
171
        }
172
173
        $paramsStringOriginal = implode(', ', $params);
174
175
        $paramsVariable = $params ? '$'.implode(
176
            ', $',
177
            array_map(
178
                static fn (ReflectionParameter $param): string => $param->getName(),
179
                $method->getParameters()
180
            )
181
        ) : '';
182
183
        $returnType = $method->getReturnType();
184
        $returnTypeString = $returnType ? $returnType->getName() : 'mixed';
185
186
        if ($returnTypeString === 'self') {
187
            $returnTypeString = '\\'.$serviceInstanceReflection->getName();
188
        }
189
190
        $params = [];
191
192
        foreach ($method->getParameters() as $param) {
193
            $paramType = $param->getType();
194
            $type = $paramType ? $paramType->getName().' ' : '';
195
            $params[] = "@param {$type}\${$param->getName()}";
196
        }
197
198
        $paramsString = implode("\n         * ", $params);
199
200
        $helperFile->fwrite("        /**\n");
201
202
        if ($paramsString) {
203
            $helperFile->fwrite("         * {$paramsString}\n");
204
        }
205
        $helperFile->fwrite("         * @return {$returnTypeString}\n");
206
        $helperFile->fwrite("         */\n");
207
208
        $helperFile->fwrite("        public static function {$method->getName()}($paramsStringOriginal): $returnTypeString\n");
209
        $helperFile->fwrite("        {\n");
210
        $helperFile->fwrite("            /** @var \\{$serviceInstanceReflection->getName()} \$instance */\n");
211
        $helperFile->fwrite("            return \$instance->{$method->getName()}($paramsVariable);\n");
212
        $helperFile->fwrite("        }\n");
213
    }
214
}
215