Issues (10)

src/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 Psr\Container\ContainerInterface;
8
use ReflectionParameter;
9
use SplFileObject;
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
52
        foreach ($finder as $file) {
53
            $className = $this->getClassNameFromFile($file->getRealPath());
54
55
            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

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

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