Passed
Pull Request — master (#107)
by Simon
01:33
created

DataObjectAnnotator::getExtensionClasses()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 1
nc 1
nop 0
dl 0
loc 3
rs 10
c 0
b 0
f 0
1
<?php
2
3
namespace SilverLeague\IDEAnnotator;
4
5
use InvalidArgumentException;
6
use LogicException;
7
use Psr\Container\NotFoundExceptionInterface;
8
use ReflectionException;
9
use SilverLeague\IDEAnnotator\Generators\DocBlockGenerator;
10
use SilverLeague\IDEAnnotator\Helpers\AnnotateClassInfo;
11
use SilverLeague\IDEAnnotator\Helpers\AnnotatePermissionChecker;
12
use SilverStripe\Control\Director;
13
use SilverStripe\Core\ClassInfo;
14
use SilverStripe\Core\Config\Config;
15
use SilverStripe\Core\Config\Configurable;
16
use SilverStripe\Core\Extensible;
17
use SilverStripe\Core\Injector\Injectable;
18
use SilverStripe\Core\Injector\Injector;
19
use SilverStripe\ORM\DB;
20
21
/**
22
 * Class DataObjectAnnotator
23
 * Generates phpdoc annotations for database fields and orm relations
24
 * so IDE's with autocompletion and property inspection will recognize properties
25
 * and relation methods.
26
 *
27
 * The annotations can be generated with dev/build with @see Annotatable
28
 * and from the @see DataObjectAnnotatorTask
29
 *
30
 * The generation is disabled by default.
31
 * It is advisable to only enable it in your local dev environment,
32
 * so the files won't change on a production server when you run dev/build
33
 *
34
 * @package IDEAnnotator/Core
35
 */
36
class DataObjectAnnotator
37
{
38
    use Injectable;
39
    use Configurable;
40
    use Extensible;
41
42
    /**
43
     * All classes that subclass Object
44
     *
45
     * @var array
46
     */
47
    protected static $extension_classes = [];
48
49
    /**
50
     * @config
51
     * Enable generation from @see Annotatable and @see DataObjectAnnotatorTask
52
     *
53
     * @var bool
54
     */
55
    private static $enabled = false;
0 ignored issues
show
introduced by
The private property $enabled is not used, and could be removed.
Loading history...
56
57
    /**
58
     * @config
59
     * Enable modules that are allowed to have generated docblocks for
60
     * DataObjects and DataExtensions
61
     *
62
     * @var array
63
     */
64
    private static $enabled_modules = ['mysite'];
0 ignored issues
show
introduced by
The private property $enabled_modules is not used, and could be removed.
Loading history...
65
66
    /**
67
     * @var AnnotatePermissionChecker
68
     */
69
    private $permissionChecker;
70
71
    /**
72
     * @var array
73
     */
74
    private $annotatableClasses = [];
75
76
    /**
77
     * DataObjectAnnotator constructor.
78
     *
79
     * @throws NotFoundExceptionInterface
80
     * @throws ReflectionException
81
     */
82
    public function __construct()
83
    {
84
        // Don't instantiate anything if annotations are not enabled.
85
        if (static::config()->get('enabled') === true && Director::isDev()) {
86
            $this->extend('beforeDataObjectAnnotator', $this);
87
88
            $this->setupExtensionClasses();
89
90
            $this->permissionChecker = Injector::inst()->get(AnnotatePermissionChecker::class);
91
92
            foreach ($this->permissionChecker->getSupportedParentClasses() as $supportedParentClass) {
93
                $this->setEnabledClasses($supportedParentClass);
94
            }
95
96
            $this->extend('afterDataObjectAnnotator', $this);
97
        }
98
    }
99
100
    /**
101
     * Get all annotatable classes from enabled modules
102
     */
103
    protected function setEnabledClasses($supportedParentClass)
104
    {
105
        foreach ((array)ClassInfo::subclassesFor($supportedParentClass) as $class) {
106
            $classInfo = new AnnotateClassInfo($class);
107
            if ($this->permissionChecker->moduleIsAllowed($classInfo->getModuleName())) {
108
                $this->annotatableClasses[$class] = $classInfo->getClassFilePath();
109
            }
110
        }
111
    }
112
113
    /**
114
     * @return array
115
     */
116
    public static function getExtensionClasses()
117
    {
118
        return self::$extension_classes;
119
    }
120
121
    /**
122
     * @param array $extension_classes
123
     */
124
    public static function setExtensionClasses($extension_classes)
125
    {
126
        self::$extension_classes = $extension_classes;
127
    }
128
129
    /**
130
     * Add another extension class
131
     * @param $extension_class
132
     */
133
    public static function pushExtensionClass($extension_class)
134
    {
135
        if (!in_array($extension_class, self::$extension_classes)) {
136
            self::$extension_classes[] = $extension_class;
137
        }
138
    }
139
140
    /**
141
     * @return boolean
142
     */
143
    public static function isEnabled()
144
    {
145
        return (bool)static::config()->get('enabled');
146
    }
147
148
    /**
149
     * Generate docblock for all subclasses of DataObjects and DataExtenions
150
     * within a module.
151
     *
152
     * @param string $moduleName
153
     * @return bool
154
     * @throws ReflectionException
155
     * @throws NotFoundExceptionInterface
156
     */
157
    public function annotateModule($moduleName)
158
    {
159
        if (!(bool)$moduleName || !$this->permissionChecker->moduleIsAllowed($moduleName)) {
160
            return false;
161
        }
162
163
        $classes = (array)$this->getClassesForModule($moduleName);
164
165
        foreach ($classes as $className => $filePath) {
166
            $this->annotateObject($className);
167
        }
168
169
        return true;
170
    }
171
172
    /**
173
     * @param $moduleName
174
     * @return array
175
     * @throws ReflectionException
176
     */
177
    public function getClassesForModule($moduleName)
178
    {
179
        $classes = [];
180
181
        foreach ($this->annotatableClasses as $class => $filePath) {
182
            $classInfo = new AnnotateClassInfo($class);
183
            if ($moduleName === $classInfo->getModuleName()) {
184
                $classes[$class] = $filePath;
185
            }
186
        }
187
188
        return $classes;
189
    }
190
191
    /**
192
     * Generate docblock for a single subclass of DataObject or DataExtenions
193
     *
194
     * @param string $className
195
     * @return bool
196
     * @throws ReflectionException
197
     * @throws NotFoundExceptionInterface
198
     */
199
    public function annotateObject($className)
200
    {
201
        if (!$this->permissionChecker->classNameIsAllowed($className)) {
202
            return false;
203
        }
204
205
        $this->writeFileContent($className);
206
207
        return true;
208
    }
209
210
    /**
211
     * @param string $className
212
     * @throws LogicException
213
     * @throws InvalidArgumentException
214
     * @throws ReflectionException
215
     */
216
    protected function writeFileContent($className)
217
    {
218
        $classInfo = new AnnotateClassInfo($className);
219
        $filePath = $classInfo->getClassFilePath();
220
221
        if (!is_writable($filePath)) {
222
            DB::alteration_message($className . ' is not writable by ' . get_current_user(), 'error');
223
        } else {
224
            $original = file_get_contents($filePath);
225
            $generated = $this->getGeneratedFileContent($original, $className);
226
227
            // we have a change, so write the new file
228
            if ($generated && $generated !== $original && $className) {
229
                file_put_contents($filePath, $generated);
230
                DB::alteration_message($className . ' Annotated', 'created');
231
            } elseif ($generated === $original && $className) {
232
                DB::alteration_message($className, 'repaired');
233
            }
234
        }
235
    }
236
237
    /**
238
     * Return the complete File content with the newly generated DocBlocks
239
     *
240
     * @param string $fileContent
241
     * @param string $className
242
     * @return mixed
243
     * @throws LogicException
244
     * @throws InvalidArgumentException
245
     * @throws ReflectionException
246
     */
247
    protected function getGeneratedFileContent($fileContent, $className)
248
    {
249
        $generator = new DocBlockGenerator($className);
250
251
        $existing = $generator->getExistingDocBlock();
252
        $generated = $generator->getGeneratedDocBlock();
253
254
        // Trim unneeded whitespaces at the end of lines for PSR-2
255
        $generated = preg_replace('/\s+$/m', '', $generated);
256
257
        if ($existing) {
258
            $fileContent = str_replace($existing, $generated, $fileContent);
259
        } else {
260
            if (class_exists($className)) {
261
                $exploded = explode("\\", $className);
262
                $classNameNew = end($exploded);
263
                $needle = "class {$classNameNew}";
264
                $replace = "{$generated}\nclass {$classNameNew}";
265
                $pos = strpos($fileContent, $needle);
266
                $fileContent = substr_replace($fileContent, $replace, $pos, strlen($needle));
267
            } else {
268
                DB::alteration_message(
269
                    "Could not find string 'class $className'. Please check casing and whitespace.",
270
                    'error'
271
                );
272
            }
273
        }
274
275
        return $fileContent;
276
    }
277
278
    /**
279
     * Named `setup` to not clash with the actual setter
280
     *
281
     * @param $extension_classes
282
     * @throws ReflectionException
283
     */
284
    protected function setupExtensionClasses()
285
    {
286
        $extension_classes = [];
287
288
        $extendableClasses = Config::inst()->getAll();
289
        // We need to check all config to see if the class is extensible
290
        // @todo find a cleaner method, this is already better than previous implementations though
291
        foreach ($extendableClasses as $key => $configClass) {
292
            if (isset($configClass['extensions']) &&
293
                count($configClass['extensions']) > 0 &&
294
                !in_array(self::$extension_classes, $configClass)
295
            ) {
296
                $extension_classes[] = ClassInfo::class_name($key);
297
            }
298
        }
299
300
        // Because the tests re-instantiate the class every time
301
        // We need to make it a unique array
302
        // Also, it's not a bad practice, making sure the array is unique
303
        $extension_classes = array_unique($extension_classes);
304
        // Keep it local until done saves roughly 1 to 2 MB of memory usage.
305
        // Keeping local I guess!
306
        static::$extension_classes = $extension_classes;
307
    }
308
}
309