Completed
Pull Request — master (#107)
by Simon
01:54
created

DataObjectAnnotator::setupExtensionClasses()   B

Complexity

Conditions 5
Paths 3

Size

Total Lines 21
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Importance

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