Completed
Pull Request — master (#107)
by Simon
03:50 queued 01:55
created

DataObjectAnnotator::annotateObject()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 9
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

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