DataObjectAnnotator::isEnabled()   A
last analyzed

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\DataObject;
20
use SilverStripe\ORM\DB;
21
use SilverStripe\ORM\FieldType\DBBoolean;
22
use SilverStripe\ORM\FieldType\DBDecimal;
23
use SilverStripe\ORM\FieldType\DBFloat;
24
use SilverStripe\ORM\FieldType\DBInt;
25
use stdClass;
26
27
/**
28
 * Class DataObjectAnnotator
29
 * Generates phpdoc annotations for database fields and orm relations
30
 * so IDE's with autocompletion and property inspection will recognize properties
31
 * and relation methods.
32
 *
33
 * The annotations can be generated with dev/build with @see Annotatable
34
 * and from the @see DataObjectAnnotatorTask
35
 *
36
 * The generation is disabled by default.
37
 * It is advisable to only enable it in your local dev environment,
38
 * so the files won't change on a production server when you run dev/build
39
 *
40
 * @package IDEAnnotator/Core
41
 */
42
class DataObjectAnnotator
43
{
44
    use Injectable;
45
    use Configurable;
46
    use Extensible;
47
48
    /**
49
     * All classes that subclass Object
50
     *
51
     * @var array
52
     */
53
    protected static $extension_classes = [];
54
55
    /**
56
     * @config
57
     * Enable generation from @see Annotatable and @see DataObjectAnnotatorTask
58
     *
59
     * @var bool
60
     */
61
    private static $enabled = false;
0 ignored issues
show
introduced by
The private property $enabled is not used, and could be removed.
Loading history...
62
63
    /**
64
     * @config
65
     * Enable modules that are allowed to have generated docblocks for
66
     * DataObjects and DataExtensions
67
     *
68
     * @var array
69
     */
70
    private static $enabled_modules = ['mysite', 'app'];
0 ignored issues
show
introduced by
The private property $enabled_modules is not used, and could be removed.
Loading history...
71
72
    /**
73
     * @var AnnotatePermissionChecker
74
     */
75
    private $permissionChecker;
76
77
    /**
78
     * @var array
79
     */
80
    private $annotatableClasses = [];
81
82
    /**
83
     * Default tagname will be @string .
84
     * All exceptions for @see DBField types are listed here
85
     *
86
     * @see generateDBTags();
87
     * @config Can be overridden via config
88
     * @var array
89
     */
90
    protected static $dbfield_tagnames = [
91
        DBInt::class     => 'int',
92
        DBBoolean::class => 'bool',
93
        DBFloat::class   => 'float',
94
        DBDecimal::class => 'float',
95
    ];
96
97
    /**
98
     * DataObjectAnnotator constructor.
99
     *
100
     * @throws NotFoundExceptionInterface
101
     * @throws ReflectionException
102
     */
103
    public function __construct()
104
    {
105
        // Don't instantiate anything if annotations are not enabled.
106
        if (static::config()->get('enabled') === true && Director::isDev()) {
107
            $this->extend('beforeDataObjectAnnotator');
108
109
            $this->setupExtensionClasses();
110
111
            $this->permissionChecker = Injector::inst()->get(AnnotatePermissionChecker::class);
112
113
            foreach ($this->permissionChecker->getSupportedParentClasses() as $supportedParentClass) {
114
                $this->setEnabledClasses($supportedParentClass);
115
            }
116
117
            $this->extend('afterDataObjectAnnotator');
118
        }
119
    }
120
121
    /**
122
     * Named `setup` to not clash with the actual setter
123
     *
124
     * Loop all extendable classes and see if they actually have extensions
125
     * If they do, add it to the array
126
     * Clean up the array of duplicates
127
     * Then save the setup of the classes in a static array, this is to save memory
128
     *
129
     * @throws ReflectionException
130
     */
131
    protected function setupExtensionClasses()
132
    {
133
        $extension_classes = [];
134
135
        $extendableClasses = Config::inst()->getAll();
136
137
        // We need to check all config to see if the class is extensible
138
        foreach ($extendableClasses as $className => $classConfig) {
139
            if (!class_exists($className)) {
140
                continue;
141
            }
142
143
            // If the class doesn't already exist in the extension classes
144
            if (in_array($className, self::$extension_classes)) {
145
                continue;
146
            }
147
148
            // And the class has extensions,
149
            $extensions = DataObject::get_extensions($className);
150
            if (!count($extensions)) {
151
                continue;
152
            }
153
154
            // Add it.
155
            $extension_classes[] = ClassInfo::class_name($className);
156
        }
157
158
        $extension_classes = array_unique($extension_classes);
159
160
        static::$extension_classes = $extension_classes;
161
    }
162
163
    /**
164
     * Get all annotatable classes from enabled modules
165
     * @param string|StdClass $supportedParentClass
166
     * @throws ReflectionException
167
     */
168
    protected function setEnabledClasses($supportedParentClass)
169
    {
170
        foreach ((array)ClassInfo::subclassesFor($supportedParentClass) as $class) {
171
            if (!class_exists($class)) {
172
                continue;
173
            }
174
            $classInfo = new AnnotateClassInfo($class);
175
            if ($this->permissionChecker->moduleIsAllowed($classInfo->getModuleName())) {
176
                $this->annotatableClasses[$class] = $classInfo->getClassFilePath();
177
            }
178
        }
179
    }
180
181
    /**
182
     * @return array
183
     */
184
    public static function getExtensionClasses()
185
    {
186
        return self::$extension_classes;
187
    }
188
189
    /**
190
     * @param array $extension_classes
191
     */
192
    public static function setExtensionClasses($extension_classes)
193
    {
194
        self::$extension_classes = $extension_classes;
195
    }
196
197
    /**
198
     * Add another extension class
199
     * False checking, because what we get might be uppercase and then lowercase
200
     * Allowing for duplicates here, to clean up later
201
     *
202
     * @param string $extension_class
203
     */
204
    public static function pushExtensionClass($extension_class)
205
    {
206
        if (!in_array($extension_class, self::$extension_classes)) {
207
            self::$extension_classes[] = $extension_class;
208
        }
209
    }
210
211
    /**
212
     * @return boolean
213
     */
214
    public static function isEnabled()
215
    {
216
        return (bool)static::config()->get('enabled');
217
    }
218
219
    /**
220
     * Generate docblock for all subclasses of DataObjects and DataExtenions
221
     * within a module.
222
     *
223
     * @param string $moduleName
224
     * @return bool
225
     * @throws ReflectionException
226
     * @throws NotFoundExceptionInterface
227
     */
228
    public function annotateModule($moduleName)
229
    {
230
        if (!(bool)$moduleName || !$this->permissionChecker->moduleIsAllowed($moduleName)) {
231
            return false;
232
        }
233
234
        $classes = (array)$this->getClassesForModule($moduleName);
235
236
        foreach ($classes as $className => $filePath) {
237
            $this->annotateObject($className);
238
        }
239
240
        return true;
241
    }
242
243
    /**
244
     * @param $moduleName
245
     * @return array
246
     * @throws ReflectionException
247
     */
248
    public function getClassesForModule($moduleName)
249
    {
250
        $classes = [];
251
252
        foreach ($this->annotatableClasses as $class => $filePath) {
253
            $classInfo = new AnnotateClassInfo($class);
254
            if ($moduleName === $classInfo->getModuleName()) {
255
                $classes[$class] = $filePath;
256
            }
257
        }
258
259
        return $classes;
260
    }
261
262
    /**
263
     * Generate docblock for a single subclass of DataObject or DataExtenions
264
     *
265
     * @param string $className
266
     * @return bool
267
     * @throws \InvalidArgumentException
268
     * @throws ReflectionException
269
     * @throws NotFoundExceptionInterface
270
     */
271
    public function annotateObject($className)
272
    {
273
        if (!$this->permissionChecker->classNameIsAllowed($className)) {
274
            return false;
275
        }
276
277
        $this->writeFileContent($className);
278
279
        return true;
280
    }
281
282
    /**
283
     * @param string $className
284
     * @throws LogicException
285
     * @throws InvalidArgumentException
286
     * @throws ReflectionException
287
     */
288
    protected function writeFileContent($className)
289
    {
290
        $classInfo = new AnnotateClassInfo($className);
291
        $filePath = $classInfo->getClassFilePath();
292
293
        if (!is_writable($filePath)) {
294
            // Unsure how to test this properly
295
            DB::alteration_message($className . ' is not writable by ' . get_current_user(), 'error');
296
        } else {
297
            $original = file_get_contents($filePath);
298
            $generated = $this->getGeneratedFileContent($original, $className);
299
300
            // we have a change, so write the new file
301
            if ($generated && $generated !== $original && $className) {
302
                file_put_contents($filePath, $generated);
303
                DB::alteration_message($className . ' Annotated', 'created');
304
            } elseif ($generated === $original && $className) {
305
                // Unsure how to test this properly
306
                DB::alteration_message($className, 'repaired');
307
            }
308
        }
309
    }
310
311
    /**
312
     * Return the complete File content with the newly generated DocBlocks
313
     *
314
     * @param string $fileContent
315
     * @param string $className
316
     * @return mixed
317
     * @throws LogicException
318
     * @throws InvalidArgumentException
319
     * @throws ReflectionException
320
     */
321
    protected function getGeneratedFileContent($fileContent, $className)
322
    {
323
        $generator = new DocBlockGenerator($className);
324
325
        $existing = $generator->getExistingDocBlock();
326
        $generated = $generator->getGeneratedDocBlock();
327
328
        // Trim unneeded whitespaces at the end of lines for PSR-2
329
        $generated = preg_replace('/\s+$/m', '', $generated);
330
331
        // $existing could be a boolean that in theory is `true`
332
        // It never is though (according to the generator's doc)
333
        if ((bool)$existing !== false) {
334
            $fileContent = str_replace($existing, $generated, $fileContent);
335
        } else {
336
            if (class_exists($className)) {
337
                $exploded = ClassInfo::shortName($className);
338
                $needle = "class {$exploded}";
339
                $replace = "{$generated}\nclass {$exploded}";
340
                $pos = strpos($fileContent, $needle);
341
                $fileContent = substr_replace($fileContent, $replace, $pos, strlen($needle));
342
            } else {
343
                DB::alteration_message(
344
                    "Could not find string 'class $className'. Please check casing and whitespace.",
345
                    'error'
346
                );
347
            }
348
        }
349
350
        return $fileContent;
351
    }
352
}
353