Completed
Pull Request — master (#107)
by Simon
01:47
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
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');
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');
97
        }
98
    }
99
100
    /**
101
     * Named `setup` to not clash with the actual setter
102
     *
103
     * @throws ReflectionException
104
     */
105
    protected function setupExtensionClasses()
106
    {
107
        $extension_classes = [];
108
109
        $extendableClasses = Config::inst()->getAll();
110
        // We need to check all config to see if the class is extensible
111
        // @todo find a cleaner method, this is already better than previous implementations though
112
        foreach ($extendableClasses as $key => $configClass) {
113
            if (isset($configClass['extensions']) &&
114
                count($configClass['extensions']) > 0 &&
115
                !in_array(self::$extension_classes, $configClass)
116
            ) {
117
                $extension_classes[] = ClassInfo::class_name($key);
118
            }
119
        }
120
121
        // Because the tests re-instantiate the class every time
122
        // We need to make it a unique array
123
        // Also, it's not a bad practice, making sure the array is unique
124
        $extension_classes = array_unique($extension_classes);
125
        // Keep it local until done saves roughly 1 to 2 MB of memory usage.
126
        // Keeping local I guess!
127
        static::$extension_classes = $extension_classes;
128
    }
129
130
    /**
131
     * Get all annotatable classes from enabled modules
132
     * @throws ReflectionException
133
     */
134
    protected function setEnabledClasses($supportedParentClass)
135
    {
136
        foreach ((array)ClassInfo::subclassesFor($supportedParentClass) as $class) {
137
            $classInfo = new AnnotateClassInfo($class);
138
            if ($this->permissionChecker->moduleIsAllowed($classInfo->getModuleName())) {
139
                $this->annotatableClasses[$class] = $classInfo->getClassFilePath();
140
            }
141
        }
142
    }
143
144
    /**
145
     * @return array
146
     */
147
    public static function getExtensionClasses()
148
    {
149
        return self::$extension_classes;
150
    }
151
152
    /**
153
     * @param array $extension_classes
154
     */
155
    public static function setExtensionClasses($extension_classes)
156
    {
157
        self::$extension_classes = $extension_classes;
158
    }
159
160
    /**
161
     * Add another extension class
162
     * @param string $extension_class
163
     */
164
    public static function pushExtensionClass($extension_class)
165
    {
166
        if (!in_array($extension_class, self::$extension_classes)) {
167
            self::$extension_classes[] = $extension_class;
168
        }
169
    }
170
171
    /**
172
     * @return boolean
173
     */
174
    public static function isEnabled()
175
    {
176
        return (bool)static::config()->get('enabled');
177
    }
178
179
    /**
180
     * Generate docblock for all subclasses of DataObjects and DataExtenions
181
     * within a module.
182
     *
183
     * @param string $moduleName
184
     * @return bool
185
     * @throws ReflectionException
186
     * @throws NotFoundExceptionInterface
187
     */
188
    public function annotateModule($moduleName)
189
    {
190
        if (!(bool)$moduleName || !$this->permissionChecker->moduleIsAllowed($moduleName)) {
191
            return false;
192
        }
193
194
        $classes = (array)$this->getClassesForModule($moduleName);
195
196
        foreach ($classes as $className => $filePath) {
197
            $this->annotateObject($className);
198
        }
199
200
        return true;
201
    }
202
203
    /**
204
     * @param $moduleName
205
     * @return array
206
     * @throws ReflectionException
207
     */
208
    public function getClassesForModule($moduleName)
209
    {
210
        $classes = [];
211
212
        foreach ($this->annotatableClasses as $class => $filePath) {
213
            $classInfo = new AnnotateClassInfo($class);
214
            if ($moduleName === $classInfo->getModuleName()) {
215
                $classes[$class] = $filePath;
216
            }
217
        }
218
219
        return $classes;
220
    }
221
222
    /**
223
     * Generate docblock for a single subclass of DataObject or DataExtenions
224
     *
225
     * @param string $className
226
     * @return bool
227
     * @throws ReflectionException
228
     * @throws NotFoundExceptionInterface
229
     */
230
    public function annotateObject($className)
231
    {
232
        if (!$this->permissionChecker->classNameIsAllowed($className)) {
233
            return false;
234
        }
235
236
        $this->writeFileContent($className);
237
238
        return true;
239
    }
240
241
    /**
242
     * @param string $className
243
     * @throws LogicException
244
     * @throws InvalidArgumentException
245
     * @throws ReflectionException
246
     */
247
    protected function writeFileContent($className)
248
    {
249
        $classInfo = new AnnotateClassInfo($className);
250
        $filePath = $classInfo->getClassFilePath();
251
252
        if (!is_writable($filePath)) {
253
            // Unsure how to test this properly
254
            DB::alteration_message($className . ' is not writable by ' . get_current_user(), 'error');
255
        } else {
256
            $original = file_get_contents($filePath);
257
            $generated = $this->getGeneratedFileContent($original, $className);
258
259
            // we have a change, so write the new file
260
            if ($generated && $generated !== $original && $className) {
261
                file_put_contents($filePath, $generated);
262
                DB::alteration_message($className . ' Annotated', 'created');
263
            } elseif ($generated === $original && $className) {
264
                // Unsure how to test this properly
265
                DB::alteration_message($className, 'repaired');
266
            }
267
        }
268
    }
269
270
    /**
271
     * Return the complete File content with the newly generated DocBlocks
272
     *
273
     * @param string $fileContent
274
     * @param string $className
275
     * @return mixed
276
     * @throws LogicException
277
     * @throws InvalidArgumentException
278
     * @throws ReflectionException
279
     */
280
    protected function getGeneratedFileContent($fileContent, $className)
281
    {
282
        $generator = new DocBlockGenerator($className);
283
284
        $existing = $generator->getExistingDocBlock();
285
        $generated = $generator->getGeneratedDocBlock();
286
287
        // Trim unneeded whitespaces at the end of lines for PSR-2
288
        $generated = preg_replace('/\s+$/m', '', $generated);
289
290
        // $existing could be a boolean that in theory is `true`
291
        // It never is though (according to the generator's doc)
292
        if ((bool)$existing !== false) {
293
            $fileContent = str_replace($existing, $generated, $fileContent);
294
        } else {
295
            if (class_exists($className)) {
296
                $exploded = ClassInfo::shortName($className);
297
                $needle = "class {$exploded}";
298
                $replace = "{$generated}\nclass {$exploded}";
299
                $pos = strpos($fileContent, $needle);
300
                $fileContent = substr_replace($fileContent, $replace, $pos, strlen($needle));
301
            } else {
302
                DB::alteration_message(
303
                    "Could not find string 'class $className'. Please check casing and whitespace.",
304
                    'error'
305
                );
306
            }
307
        }
308
309
        return $fileContent;
310
    }
311
}
312