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

DataObjectAnnotator   B

Complexity

Total Complexity 37

Size/Duplication

Total Lines 250
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
dl 0
loc 250
rs 8.6
c 0
b 0
f 0
wmc 37

11 Methods

Rating   Name   Duplication   Size   Complexity  
A setEnabledClasses() 0 6 3
A getClassesForModule() 0 12 3
A getExtensionClasses() 0 3 1
A setExtensionClasses() 0 3 1
B getGeneratedFileContent() 0 36 5
A annotateObject() 0 9 2
C __construct() 0 26 8
A isEnabled() 0 3 1
B writeFileContent() 0 17 7
A pushExtensionClass() 0 4 2
A annotateModule() 0 13 4
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
            $extendableClasses = Config::inst()->getAll();
74
            // We need to check all config to see if the class is extensible
75
            // @todo find a cleaner method, this is already better than previous implementations though
76
            foreach ($extendableClasses as $key => $configClass) {
77
                if (isset($configClass['extensions']) &&
78
                    count($configClass['extensions']) > 0 &&
79
                    !in_array(self::$extension_classes, $configClass)) {
80
                    $extension_classes[] = ClassInfo::class_name($key);
81
                }
82
            }
83
84
            // Because the tests re-instantiate the class every time
85
            // We need to make it a unique array
86
            // Also, it's not a bad practice, making sure the array is unique
87
            $extension_classes = array_unique($extension_classes);
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $extension_classes does not seem to be defined for all execution paths leading up to this point.
Loading history...
88
            // Keep it local until done saves roughly 1 to 2 MB of memory usage.
89
            // Keeping local I guess!
90
            static::$extension_classes = $extension_classes;
91
92
            $this->permissionChecker = Injector::inst()->get(AnnotatePermissionChecker::class);
93
            foreach ($this->permissionChecker->getSupportedParentClasses() as $supportedParentClass) {
94
                $this->setEnabledClasses($supportedParentClass);
95
            }
96
        }
97
    }
98
99
    /**
100
     * Get all annotatable classes from enabled modules
101
     */
102
    protected function setEnabledClasses($supportedParentClass)
103
    {
104
        foreach ((array)ClassInfo::subclassesFor($supportedParentClass) as $class) {
105
            $classInfo = new AnnotateClassInfo($class);
106
            if ($this->permissionChecker->moduleIsAllowed($classInfo->getModuleName())) {
107
                $this->annotatableClasses[$class] = $classInfo->getClassFilePath();
108
            }
109
        }
110
    }
111
112
    /**
113
     * @return array
114
     */
115
    public static function getExtensionClasses()
116
    {
117
        return self::$extension_classes;
118
    }
119
120
    /**
121
     * @param array $extension_classes
122
     */
123
    public static function setExtensionClasses($extension_classes)
124
    {
125
        self::$extension_classes = $extension_classes;
126
    }
127
128
    /**
129
     * Add another extension class
130
     * @param $extension_class
131
     */
132
    public static function pushExtensionClass($extension_class)
133
    {
134
        if (!in_array($extension_class, self::$extension_classes)) {
135
            self::$extension_classes[] = $extension_class;
136
        }
137
    }
138
139
    /**
140
     * @return boolean
141
     */
142
    public static function isEnabled()
143
    {
144
        return (bool)static::config()->get('enabled');
145
    }
146
147
    /**
148
     * Generate docblock for all subclasses of DataObjects and DataExtenions
149
     * within a module.
150
     *
151
     * @param string $moduleName
152
     * @return bool
153
     * @throws ReflectionException
154
     * @throws NotFoundExceptionInterface
155
     */
156
    public function annotateModule($moduleName)
157
    {
158
        if (!(bool)$moduleName || !$this->permissionChecker->moduleIsAllowed($moduleName)) {
159
            return false;
160
        }
161
162
        $classes = (array)$this->getClassesForModule($moduleName);
163
164
        foreach ($classes as $className => $filePath) {
165
            $this->annotateObject($className);
166
        }
167
168
        return true;
169
    }
170
171
    /**
172
     * @param $moduleName
173
     * @return array
174
     * @throws ReflectionException
175
     */
176
    public function getClassesForModule($moduleName)
177
    {
178
        $classes = [];
179
180
        foreach ($this->annotatableClasses as $class => $filePath) {
181
            $classInfo = new AnnotateClassInfo($class);
182
            if ($moduleName === $classInfo->getModuleName()) {
183
                $classes[$class] = $filePath;
184
            }
185
        }
186
187
        return $classes;
188
    }
189
190
    /**
191
     * Generate docblock for a single subclass of DataObject or DataExtenions
192
     *
193
     * @param string $className
194
     * @return bool
195
     * @throws ReflectionException
196
     * @throws NotFoundExceptionInterface
197
     */
198
    public function annotateObject($className)
199
    {
200
        if (!$this->permissionChecker->classNameIsAllowed($className)) {
201
            return false;
202
        }
203
204
        $this->writeFileContent($className);
205
206
        return true;
207
    }
208
209
    /**
210
     * @param string $className
211
     * @throws LogicException
212
     * @throws InvalidArgumentException
213
     * @throws ReflectionException
214
     */
215
    protected function writeFileContent($className)
216
    {
217
        $classInfo = new AnnotateClassInfo($className);
218
        $filePath = $classInfo->getClassFilePath();
219
220
        if (!is_writable($filePath)) {
221
            DB::alteration_message($className . ' is not writable by ' . get_current_user(), 'error');
222
        } else {
223
            $original = file_get_contents($filePath);
224
            $generated = $this->getGeneratedFileContent($original, $className);
225
226
            // we have a change, so write the new file
227
            if ($generated && $generated !== $original && $className) {
228
                file_put_contents($filePath, $generated);
229
                DB::alteration_message($className . ' Annotated', 'created');
230
            } elseif ($generated === $original && $className) {
231
                DB::alteration_message($className, 'repaired');
232
            }
233
        }
234
    }
235
236
    /**
237
     * Return the complete File content with the newly generated DocBlocks
238
     *
239
     * @param string $fileContent
240
     * @param string $className
241
     * @return mixed
242
     * @throws LogicException
243
     * @throws InvalidArgumentException
244
     * @throws ReflectionException
245
     */
246
    protected function getGeneratedFileContent($fileContent, $className)
247
    {
248
        $generator = new DocBlockGenerator($className);
249
250
        $existing = $generator->getExistingDocBlock();
251
        $generated = $generator->getGeneratedDocBlock();
252
253
        // Trim unneeded whitespaces at the end of lines for PSR-2
254
        $generated = preg_replace('/\s+$/m', '', $generated);
255
256
        if ($existing) {
257
            $fileContent = str_replace($existing, $generated, $fileContent);
258
        } else {
259
            $needle = "class {$className}";
260
            $replace = "{$generated}\nclass {$className}";
261
            $pos = strpos($fileContent, $needle);
262
            if ($pos !== false) {
0 ignored issues
show
introduced by
The condition $pos !== false can never be false.
Loading history...
263
                $fileContent = substr_replace($fileContent, $replace, $pos, strlen($needle));
264
            } else {
265
                if (strrpos($className, "\\") !== false && class_exists($className)) {
266
                    $exploded = explode("\\", $className);
267
                    $classNameNew = end($exploded);
268
                    $needle = "class {$classNameNew}";
269
                    $replace = "{$generated}\nclass {$classNameNew}";
270
                    $pos = strpos($fileContent, $needle);
271
                    $fileContent = substr_replace($fileContent, $replace, $pos, strlen($needle));
272
                } else {
273
                    DB::alteration_message(
274
                        "Could not find string 'class $className'. Please check casing and whitespace.",
275
                        'error'
276
                    );
277
                }
278
            }
279
        }
280
281
        return $fileContent;
282
    }
283
}
284