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
![]() |
|||
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
|
|||
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 |