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

DataObjectAnnotator::setupExtensionClasses()   B

Complexity

Conditions 5
Paths 3

Size

Total Lines 23
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Importance

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