Completed
Pull Request — master (#98)
by Robbie
02:47 queued 01:00
created

DataObjectAnnotator   A

Complexity

Total Complexity 29

Size/Duplication

Total Lines 192
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
dl 0
loc 192
rs 10
c 0
b 0
f 0
wmc 29

8 Methods

Rating   Name   Duplication   Size   Complexity  
A setEnabledClasses() 0 6 3
A getClassesForModule() 0 12 3
B getGeneratedFileContent() 0 34 5
A annotateObject() 0 9 2
A __construct() 0 7 4
A isEnabled() 0 3 1
B writeFileContent() 0 17 7
A annotateModule() 0 13 4
1
<?php
2
3
namespace SilverLeague\IDEAnnotator;
4
5
use Psr\Container\NotFoundExceptionInterface;
6
use ReflectionException;
7
use SilverStripe\Control\Director;
8
use SilverStripe\Core\ClassInfo;
9
use SilverStripe\Core\Config\Configurable;
10
use SilverStripe\Core\Extensible;
11
use SilverStripe\Core\Injector\Injectable;
12
use SilverStripe\Core\Injector\Injector;
13
use SilverStripe\ORM\DB;
14
15
/**
16
 * Class DataObjectAnnotator
17
 * Generates phpdoc annotations for database fields and orm relations
18
 * so IDE's with autocompletion and property inspection will recognize properties and relation methods.
19
 *
20
 * The annotations can be generated with dev/build with @see Annotatable
21
 * and from the @see DataObjectAnnotatorTask
22
 *
23
 * The generation is disabled by default.
24
 * It is advisable to only enable it in your local dev environment,
25
 * so the files won't change on a production server when you run dev/build
26
 *
27
 * @package IDEAnnotator/Core
28
 */
29
class DataObjectAnnotator
30
{
31
    use Injectable;
32
    use Configurable;
33
    use Extensible;
34
35
    /**
36
     * @config
37
     * Enable generation from @see Annotatable and @see DataObjectAnnotatorTask
38
     * @var bool
39
     */
40
    private static $enabled = false;
1 ignored issue
show
Unused Code introduced by
The property $enabled is not used and could be removed.

This check marks private properties in classes that are never used. Those properties can be removed.

Loading history...
41
42
    /**
43
     * @config
44
     * Enable modules that are allowed to have generated docblocks for DataObjects and DataExtensions
45
     * @var array
46
     */
47
    private static $enabled_modules = ['mysite'];
1 ignored issue
show
Unused Code introduced by
The property $enabled_modules is not used and could be removed.

This check marks private properties in classes that are never used. Those properties can be removed.

Loading history...
48
49
    /**
50
     * @var AnnotatePermissionChecker
51
     */
52
    private $permissionChecker;
53
54
    /**
55
     * @var array
56
     */
57
    private $annotatableClasses = [];
58
59
    /**
60
     * DataObjectAnnotator constructor.
61
     * @throws NotFoundExceptionInterface
62
     */
63
    public function __construct()
64
    {
65
        // Don't instantiate anything if annotations are not enabled.
66
        if (static::config()->get('enabled') === true && Director::isDev()) {
67
            $this->permissionChecker = Injector::inst()->get(AnnotatePermissionChecker::class);
68
            foreach ($this->permissionChecker->getSupportedParentClasses() as $supportedParentClass) {
69
                $this->setEnabledClasses($supportedParentClass);
70
            }
71
        }
72
    }
73
74
    /**
75
     * Get all annotatable classes from enabled modules
76
     */
77
    protected function setEnabledClasses($supportedParentClass)
78
    {
79
        foreach ((array)ClassInfo::subclassesFor($supportedParentClass) as $class) {
80
            $classInfo = new AnnotateClassInfo($class);
81
            if ($this->permissionChecker->moduleIsAllowed($classInfo->getModuleName())) {
82
                $this->annotatableClasses[$class] = $classInfo->getClassFilePath();
83
            }
84
        }
85
    }
86
87
    /**
88
     * @return boolean
89
     */
90
    public static function isEnabled()
91
    {
92
        return (bool)static::config()->get('enabled');
93
    }
94
95
    /**
96
     * Generate docblock for all subclasses of DataObjects and DataExtenions
97
     * within a module.
98
     *
99
     * @param string $moduleName
100
     * @return bool
101
     */
102
    public function annotateModule($moduleName)
103
    {
104
        if (!(bool)$moduleName || !$this->permissionChecker->moduleIsAllowed($moduleName)) {
105
            return false;
106
        }
107
108
        $classes = (array)$this->getClassesForModule($moduleName);
109
110
        foreach ($classes as $className => $filePath) {
111
            $this->annotateObject($className);
112
        }
113
114
        return true;
115
    }
116
117
    /**
118
     * @param $moduleName
119
     * @return array
120
     * @throws ReflectionException
121
     */
122
    public function getClassesForModule($moduleName)
123
    {
124
        $classes = [];
125
126
        foreach ($this->annotatableClasses as $class => $filePath) {
127
            $classInfo = new AnnotateClassInfo($class);
128
            if ($moduleName === $classInfo->getModuleName()) {
129
                $classes[$class] = $filePath;
130
            }
131
        }
132
133
        return $classes;
134
    }
135
136
    /**
137
     * Generate docblock for a single subclass of DataObject or DataExtenions
138
     *
139
     * @param string $className
140
     * @return bool
141
     * @throws ReflectionException
142
     */
143
    public function annotateObject($className)
144
    {
145
        if (!$this->permissionChecker->classNameIsAllowed($className)) {
146
            return false;
147
        }
148
149
        $this->writeFileContent($className);
150
151
        return true;
152
    }
153
154
    /**
155
     * @param string $className
156
     * @throws ReflectionException
157
     */
158
    protected function writeFileContent($className)
159
    {
160
        $classInfo = new AnnotateClassInfo($className);
161
        $filePath = $classInfo->getClassFilePath();
162
163
        if (!is_writable($filePath)) {
164
            DB::alteration_message($className . ' is not writable by ' . get_current_user(), 'error');
165
        } else {
166
            $original = file_get_contents($filePath);
167
            $generated = $this->getGeneratedFileContent($original, $className);
168
169
            // we have a change, so write the new file
170
            if ($generated && $generated !== $original && $className) {
171
                file_put_contents($filePath, $generated);
172
                DB::alteration_message($className . ' Annotated', 'created');
173
            } elseif ($generated === $original && $className) {
174
                DB::alteration_message($className);
175
            }
176
        }
177
    }
178
179
    /**
180
     * Return the complete File content with the newly generated DocBlocks
181
     *
182
     * @param string $fileContent
183
     * @param string $className
184
     * @return mixed
185
     * @throws ReflectionException
186
     */
187
    protected function getGeneratedFileContent($fileContent, $className)
188
    {
189
        $generator = new DocBlockGenerator($className);
190
191
        $existing = $generator->getExistingDocBlock();
192
        $generated = $generator->getGeneratedDocBlock();
193
194
        if ($existing) {
195
            $fileContent = str_replace($existing, $generated, $fileContent);
196
        } else {
197
            $needle = "class {$className}";
198
            $replace = "{$generated}\nclass {$className}";
199
            $pos = strpos($fileContent, $needle);
200
            if ($pos !== false) {
201
                $fileContent = substr_replace($fileContent, $replace, $pos, strlen($needle));
202
            } else {
203
                if (strrpos($className, "\\") !== false && class_exists($className)) {
204
                    $exploded = explode("\\", $className);
205
                    $classNameNew = end($exploded);
206
                    $needle = "class {$classNameNew}";
207
                    $replace = "{$generated}\nclass {$classNameNew}";
208
                    $pos = strpos($fileContent, $needle);
209
                    $fileContent = substr_replace($fileContent, $replace, $pos, strlen($needle));
210
                    DB::alteration_message('Found namespaced Class: ' . $classNameNew);
211
                } else {
212
                    DB::alteration_message(
213
                        "Could not find string 'class $className'. Please check casing and whitespace.",
214
                        'error'
215
                    );
216
                }
217
            }
218
        }
219
220
        return $fileContent;
221
    }
222
}
223