ModelAnnotationsTask   A
last analyzed

Complexity

Total Complexity 40

Size/Duplication

Total Lines 406
Duplicated Lines 0 %

Test Coverage

Coverage 87.7%

Importance

Changes 0
Metric Value
eloc 163
dl 0
loc 406
ccs 107
cts 122
cp 0.877
rs 9.2
c 0
b 0
f 0
wmc 40

9 Methods

Rating   Name   Duplication   Size   Complexity  
A setUtil() 0 5 1
A __construct() 0 12 2
F run() 0 170 29
A setRequest() 0 5 1
A getDataClasses() 0 13 1
A printError() 0 10 2
A getRenderer() 0 3 1
A getConfigVarValue() 0 9 2
A setLogger() 0 5 1

How to fix   Complexity   

Complex Class

Complex classes like ModelAnnotationsTask often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use ModelAnnotationsTask, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
namespace CSoellinger\SilverStripe\ModelAnnotations\Task;
4
5
use CSoellinger\SilverStripe\ModelAnnotations\Handler\DataClassHandler;
6
use CSoellinger\SilverStripe\ModelAnnotations\Util\Util;
7
use CSoellinger\SilverStripe\ModelAnnotations\View\DataClassTaskView;
8
use Error;
9
use Exception;
10
use Monolog\Logger;
11
use Psr\Log\LoggerInterface;
12
use RuntimeException;
13
use SilverStripe\Control\Director;
14
use SilverStripe\Control\HTTPRequest;
15
use SilverStripe\Core\ClassInfo;
16
use SilverStripe\Core\Config\Configurable;
17
use SilverStripe\Core\Environment;
18
use SilverStripe\Core\Injector\Injector;
19
use SilverStripe\Dev\BuildTask;
20
use SilverStripe\Dev\CliDebugView;
21
use SilverStripe\Dev\DebugView;
22
use SilverStripe\ORM\DataObject;
23
use SilverStripe\Security\Permission;
24
use Psl\Str;
25
26
/**
27
 * This task generates annotations for your silver stripe model. This is mostly
28
 * helpful for the IDE autocompletion. Please always make a dry run before
29
 * really writing to the files.
30
 */
31
class ModelAnnotationsTask extends BuildTask
32
{
33
    use Configurable;
34
35
    /**
36
     * @var string {@inheritDoc}
37
     */
38
    protected $title = 'Model Annotations Generator';
39
40
    /**
41
     * @var string {@inheritDoc}
42
     */
43
    protected $description = "
44
        Add ide annotations for dataobject models. This is helpful to get auto
45
        completions for db fields and relations in your ide (most should
46
        support it). This task (over)write files so it's always a good idea to
47
        make a dryRun and/or backup files.
48
49
		Parameters (optional):
50
        - dataClass: Generate annotations only for one class. If not set all found data object classes will be used.
51
		- createBackupFile: Create a backup before writing a file. (default: FALSE)
52
		- addUseStatements: Add use statements for data types which are not declared (default: FALSE)
53
		- dryRun: Only print changes and don't write file (default: TRUE)
54
		- quiet: No outputs (default: FALSE)
55
	";
56
57
    /**
58
     * @var string {@inheritDoc}
59
     */
60
    private static string $segment = 'ModelAnnotationsTask';
61
62
    /**
63
     * @var string[] site tree fields which are ignored if the data object extends
64
     *               from it
65
     */
66
    private static array $siteTreeFields = [
67
        'URLSegment',
68
        'Title',
69
        'MenuTitle',
70
        'Content',
71
        'MetaDescription',
72
        'ExtraMeta',
73
        'ReportClass',
74
        'Sort',
75
        'ShowInMenus',
76
        'ShowInSearch',
77
        'HasBrokenFile',
78
        'HasBrokenLink',
79
        'ViewerGroups',
80
        'EditorGroups',
81
        'Parent',
82
        'BackLinks',
83
        'VirtualPages',
84
    ];
85
86
    /**
87
     * @var string[] custom field which will be ignored
88
     */
89
    private static array $ignoreFields = [
90
        'LinkTracking',
91
        'FileTracking',
92
    ];
93
94
    /**
95
     * @var bool Create a backup file before writing the model. You
96
     *           can set this variable also as $_GET var.
97
     */
98
    private static bool $createBackupFile = false;
99
100
    /**
101
     * @var bool Only print what normally would be written to file. You
102
     *           can set this variable also as $_GET var.
103
     */
104
    private static bool $dryRun = true;
105
106
    /**
107
     * @var bool If we set this to true it will add use statements for data
108
     *           types. It also shortens down the data type inside php doc. You
109
     *           can set this variable also as $_GET var.
110
     */
111
    private static bool $addUseStatements = false;
112
113
    /**
114
     * @var bool Prints no output on true.
115
     */
116
    private static bool $quiet = false;
117
118
    /**
119
     * @var array<string,string> Auto injected dependencies
120
     */
121
    private static $dependencies = [
122
        'logger' => '%$' . LoggerInterface::class,
123
        'util' => '%$' . Util::class,
124
        'request' => '%$' . HTTPRequest::class,
125
    ];
126
127
    /**
128
     * @var Logger Logger
129
     * @psalm-suppress PropertyNotSetInConstructor
130
     */
131
    private $logger;
132
133
    /**
134
     * @var Util Util helper class
135
     * @psalm-suppress PropertyNotSetInConstructor
136
     */
137
    private $util;
138
139
    /**
140
     * @var HTTPRequest Silverstripe http request class
141
     * @psalm-suppress PropertyNotSetInConstructor
142
     */
143
    private $request;
144
145
    /**
146
     * @var DebugView|CliDebugView Custom injected renderer. Depends if
147
     * environment is on cli or not.
148
     */
149
    private $renderer;
150
151 17
    public function __construct()
152
    {
153 17
        parent::__construct();
154
155 17
        if (Director::is_cli() === true) {
156
            /** @var CliDebugView $renderer */
157 17
            $renderer = Injector::inst()->get(CliDebugView::class);
158 17
            $this->renderer = $renderer;
159
        } else {
160
            /** @var DebugView $renderer */
161 1
            $renderer = Injector::inst()->get(DebugView::class);
162 1
            $this->renderer = $renderer;
163
        }
164
    }
165
166
    /**
167
     * Task will search for all data object classes and try to add necessary
168
     * annotations. If there are already some defined it should leave them
169
     * and just add the annotations which not exists.
170
     *
171
     * @param HTTPRequest $request
172
     */
173 10
    public function run($request)
174
    {
175 10
        $quiet = $this->getConfigVarValue('quiet');
176
177 10
        if ($quiet === false) {
178 9
            echo $this->getRenderer()->renderHeader();
179
        }
180
181 10
        if (Director::isDev() === false) {
182 2
            $error = 'You can run this task only inside a dev environment. ';
183 2
            $error .= 'Your environment is: ' . Director::get_environment_type();
184
185 2
            $this->printError($error);
186
        }
187
188 8
        if (Permission::check('ADMIN') === false && Director::is_cli() === false) {
189 1
            $this->printError('Inside browser only admins are allowed to run this task.');
190
        }
191
192
        // Set max time and memory limit
193
        /** @psalm-suppress UndefinedDocblockClass */
194 7
        Environment::increaseTimeLimitTo();
195
196
        /** @psalm-suppress UndefinedDocblockClass */
197 7
        Environment::setMemoryLimitMax(-1);
198
199
        /** @psalm-suppress UndefinedDocblockClass */
200 7
        Environment::increaseMemoryLimitTo(-1);
201
202
        // Check config and get vars. Get vars will overrule the config
203 7
        $dryRun = $this->getConfigVarValue('dryRun');
204 7
        $addUseStatements = $this->getConfigVarValue('addUseStatements');
205 7
        $createBackupFile = $this->getConfigVarValue('createBackupFile');
206
207
        /** @var string $dataClass */
208 7
        $dataClass = $request->getVar('dataClass');
209
210 7
        if ($quiet === false) {
211 7
            $paramsText = '| ' . implode(' | ', [
212 7
                'dataClass: ' . ($dataClass ?: 'All'),
213 7
                'dryRun: ' . ($dryRun ? 'true' : 'false'),
214 7
                'addUseStatements: ' . ($addUseStatements ? 'true' : 'false'),
215 7
                'createBackupFile: ' . ($createBackupFile ? 'true' : 'false'),
216
                'quiet: false',
217
            ]) . ' |';
218
219 7
            echo $this->getRenderer()->renderInfo('Params', $paramsText);
220
        }
221
222 7
        $dataClass = strtolower($dataClass);
223 7
        $dataClasses = $this->getDataClasses();
224
225 7
        if ($dataClass !== '') {
226 7
            if (isset($dataClasses[$dataClass]) === false) {
227 1
                $this->printError('Data class "' . $dataClass . '" does not exist');
228
            }
229
230 6
            $dataClasses = [$dataClasses[$dataClass]];
231
        }
232
233 6
        foreach ($dataClasses as $index => $fqn) {
234
            try {
235
                /** @var DataClassHandler $dataClassHandler */
236 6
                $dataClassHandler = Injector::inst()->createWithArgs(DataClassHandler::class, [$fqn]);
237 6
                $dataClassAst = $dataClassHandler->getAst();
238
239 6
                if ($dataClassAst === null) {
240
                    continue;
241
                }
242
243 6
                if ($quiet === false) {
244
                    $dataClassHandler
245 6
                        ->getRenderer()
246 6
                        ->renderHeader($fqn, $dataClassHandler->getFile()->getPath())
247
                    ;
248
                }
249
250
                // Update file content with missing use statements
251 6
                $missingUseStatements = $dataClassHandler->getMissingUseStatements();
252 6
                if (count($missingUseStatements) > 0) {
253 1
                    $atLine = 2;
254 1
                    $useStatements = $dataClassHandler->getFile()->getUseStatementsFromAst();
255
256 1
                    if (count($useStatements) > 0) {
257 1
                        $lastUse = end($useStatements);
258
                        /** @var int $lineno */
259 1
                        $lineno = $lastUse->lineno;
260 1
                        $atLine = $lineno + 1;
261
                    } else {
262
                        $namespaceAst = $dataClassHandler->getFile()->getNamespaceAst();
263
264
                        if ($namespaceAst !== null) {
265
                            /** @var int $lineno */
266
                            $lineno = $namespaceAst->lineno;
267
                            $atLine = $lineno + 1;
268
                        }
269
                    }
270
271
                    $dataClassHandler
272 1
                        ->getFile()
273 1
                        ->addText(implode(PHP_EOL, $missingUseStatements), $atLine)
274
                    ;
275
                }
276
277 6
                $modelProperties = $dataClassHandler->getModelProperties();
278 6
                $modelMethods = $dataClassHandler->getModelMethods();
279
280 6
                if (count($modelProperties) > 0 || count($modelMethods) > 0) {
281 5
                    $oldPhpDoc = $dataClassHandler->getClassPhpDoc();
282 5
                    $newPhpDoc = $dataClassHandler->generateClassPhpDoc();
283
284 5
                    if ($oldPhpDoc === '') {
285
                        /** @var int $lineno */
286 3
                        $lineno = $dataClassAst->lineno;
287 3
                        $atLine = $lineno + count($missingUseStatements);
288
289
                        $dataClassHandler
290 3
                            ->getFile()
291 3
                            ->addText($newPhpDoc, $atLine)
292
                        ;
293
                    } else {
294 2
                        $dataClassHandler->getFile()->contentReplace($oldPhpDoc, $newPhpDoc);
295
                    }
296
297 5
                    if ($dryRun === false) {
298 1
                        $dataClassHandler->getFile()->write();
299
                    }
300
                }
301
302 6
                if ($quiet === false) {
303 6
                    $dataClassHandler->getRenderer()->renderMessage('Generating annotations done');
304
305 6
                    if ($dryRun === true) {
306
                        $dataClassHandler
307 5
                            ->getRenderer()
308 5
                            ->renderSource(
309 5
                                $dataClassHandler->getFile()->getPath(),
310 5
                                $dataClassHandler->getFile()->getContent()
311
                            )
312
                        ;
313
314 5
                        if ($index !== array_key_last($dataClasses)) {
315 6
                            $dataClassHandler->getRenderer()->renderHr();
316
                        }
317
                    }
318
                }
319
            } catch (\Throwable $th) {
320
                $message = 'Error generating annotations';
321
322
                /** @var DataClassTaskView $view */
323
                $view = Injector::inst()->get(DataClassTaskView::class);
324
325
                if ($quiet === false) {
326
                    $view
327
                        ->renderHeader($fqn, $this->util->fileByFqn($fqn))
328
                        ->renderMessage($message, 'error');
329
330
                    if ($index !== array_key_last($dataClasses)) {
331
                        $view->renderHr();
332
                    }
333
                } else {
334
                    $this->logger->error($message);
335
                }
336
337
                $this->logger->error($th->__toString());
338
            }
339
        }
340
341 6
        if ($quiet === false) {
342 6
            echo $this->getRenderer()->renderFooter();
343
        }
344
    }
345
346
    /**
347
     * Set http request dependency.
348
     */
349 17
    public function setRequest(HTTPRequest $request): self
350
    {
351 17
        $this->request = $request;
352
353 17
        return $this;
354
    }
355
356
    /**
357
     * Set logger dependency.
358
     */
359 17
    public function setLogger(Logger $logger): self
360
    {
361 17
        $this->logger = $logger;
362
363 17
        return $this;
364
    }
365
366
    /**
367
     * Set util dependency.
368
     */
369 17
    public function setUtil(Util $util): self
370
    {
371 17
        $this->util = $util;
372
373 17
        return $this;
374
    }
375
376
    /**
377
     * Helper to get config value which can be overruled by get var.
378
     */
379 10
    public function getConfigVarValue(string $key): bool
380
    {
381 10
        $value = (bool) ($this->request->getVar($key) ?? self::config()->get($key));
382
383 10
        if ($value !== (bool) self::config()->get($key)) {
384 6
            self::config()->set($key, $value);
385
        }
386
387 10
        return $value;
388
    }
389
390
    /**
391
     * Get all sub classes for data object. Normally this should be all models. Vendor
392
     * models will be filtered out.
393
     *
394
     * @return string[]
395
     */
396 7
    private function getDataClasses(): array
397
    {
398
        /** @var string[] */
399 7
        $dataClasses = ClassInfo::subclassesFor(DataObject::class);
400
401
        // Exclude all classes from vendor
402 7
        return array_filter($dataClasses, function (string $dataClass) {
403 7
            $file = $this->util->fileByFqn($dataClass);
404
            /** @var string $basePath */
405 7
            $basePath = BASE_PATH;
406 7
            $vendorPath = $basePath . DIRECTORY_SEPARATOR . 'vendor';
407
408 7
            return Str\starts_with($file, $vendorPath) === false;
409
        });
410
    }
411
412
    /**
413
     * Print an error an exit.
414
     *
415
     * @param string $error Error message
416
     */
417 4
    private function printError(string $error): void
418
    {
419 4
        if ($this->getConfigVarValue('quiet') === true) {
420 1
            throw new Error($error);
421
        }
422
423 3
        echo $this->getRenderer()->renderError('GET /dev/tasks/ModelAnnotationsTask', 1, $error, __FILE__, 0);
424 3
        echo $this->getRenderer()->renderFooter();
425
426 3
        throw new Error($error);
427
    }
428
429
    /**
430
     * Get renderer depending if we are on a cli or not
431
     *
432
     * @return DebugView|CliDebugView
433
     */
434 9
    private function getRenderer()
435
    {
436 9
        return $this->renderer;
437
    }
438
}
439