├── .codeclimate.yml
├── .editorconfig
├── .gitattributes
├── .github
└── workflows
│ └── ci.yml
├── .gitignore
├── .scrutinizer.yml
├── CONTRIBUTING.md
├── Changelog.md
├── LICENSE.md
├── README.md
├── _config
└── config.yml
├── codecov.yml
├── composer.json
├── docs
└── en
│ ├── CodeOfConduct.md
│ ├── Index.md
│ └── Installation.md
├── phpcs.xml.dist
├── phpunit.xml.dist
└── src
├── DataObjectAnnotator.php
├── Extensions
└── Annotatable.php
├── Generators
├── AbstractTagGenerator.php
├── ControllerTagGenerator.php
├── DocBlockGenerator.php
└── OrmTagGenerator.php
├── Helpers
├── AnnotateClassInfo.php
└── AnnotatePermissionChecker.php
├── Reflection
└── ShortNameResolver.php
└── Tasks
└── DataObjectAnnotatorTask.php
/.codeclimate.yml:
--------------------------------------------------------------------------------
1 | ---
2 | engines:
3 | duplication:
4 | enabled: true
5 | config:
6 | languages:
7 | - php
8 | fixme:
9 | enabled: true
10 | phpmd:
11 | enabled: true
12 | ratings:
13 | paths:
14 | - "**.php"
15 | exclude_paths:
16 | - docs/*
17 | - tests/*
18 |
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | # For more information about the properties used in this file,
2 | # please see the EditorConfig documentation:
3 | # http://editorconfig.org
4 |
5 | [*]
6 | charset = utf-8
7 | end_of_line = lf
8 | indent_size = 4
9 | indent_style = space
10 | insert_final_newline = true
11 | trim_trailing_whitespace = true
12 |
13 | [{*.yml,package.json}]
14 | indent_size = 2
15 |
16 | # The indent size used in the package.json file cannot be changed:
17 | # https://github.com/npm/npm/pull/3180#issuecomment-16336516
18 |
--------------------------------------------------------------------------------
/.gitattributes:
--------------------------------------------------------------------------------
1 | /tests export-ignore
2 | /.travis.yml export-ignore
3 | /phpunit.xml export-ignore
4 |
--------------------------------------------------------------------------------
/.github/workflows/ci.yml:
--------------------------------------------------------------------------------
1 | name: CI
2 |
3 | on:
4 | push:
5 | pull_request:
6 | workflow_dispatch:
7 |
8 | jobs:
9 | ci:
10 | name: CI
11 | uses: silverstripe/gha-ci/.github/workflows/ci.yml@v1
12 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | docs/en/apidocs
2 | .idea
3 | .env
4 | .htaccess
5 | !docs/en/*.md
6 | /mysite
7 | /*.php
8 | composer.lock
9 | vendor
10 | web.config
11 | install-frameworkmissing.html
12 | /assets
13 | /resources
14 | .phpunit.result.cache
15 | /public
16 | /app
17 |
--------------------------------------------------------------------------------
/.scrutinizer.yml:
--------------------------------------------------------------------------------
1 | checks:
2 | php: true
3 |
4 | build:
5 | nodes:
6 | analysis:
7 | tests:
8 | override: [php-scrutinizer-run]
9 |
10 | filter:
11 | paths: ["src/*", "tests/*"]
12 | excluded_paths:
13 | - "tests/mock/"
14 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | #Contributing
2 |
3 | Yes, please! But please adhere to the following rules, to keep everything neat and tidy.
4 |
5 | #Branching and forking
6 |
7 | When you want to contribute, please first create a fork of the master release first. At the time of writing, this is version `1.0.2`
8 |
9 | On your own branch, you are free to work as you wish of course, but preferably, follow the rules of GitFlow. With one exception. If you plan to create a new feature you want to make into a pull request, don't create a feature branch, but create a pulls branch.
10 | e.g. if you want to implement your awesome feature which can be summarised as "My Awesome Annotation", create a branch named "pulls/my-awesome-annotation".
11 | If you want to fix an issue mentioned in issues, create a branch named "pulls/issue-{#}-description-of-issue". Where {#} should be replaced with the issue number ofcourse.
12 |
13 | In case of the latter, please tag your commit with the issue number, by simply starting with `issue #15`, followed by a description of the issue that's fixed. This will tell github, your commit is linked to that specific issue.
14 |
15 | Please try to keep everything in one commit. If needed, squash and force-push.
16 |
17 | Rules above do not apply for collaborators/owner, who work on feature branches and/or hotfixes directly. (We have awesomeness powers)
18 |
19 | #Code style
20 |
21 | Please adhere to the [SilverStripe CodeOfConduct](CodeOfConduct.md).
22 |
--------------------------------------------------------------------------------
/Changelog.md:
--------------------------------------------------------------------------------
1 | # Pre 1.0
2 |
3 | * Write docblocks to the head of a single class
4 |
5 | # 1.0
6 |
7 | * use the start/end tags to detect changes.
8 |
9 | # 1.0.1 to 1.0.4
10 |
11 | * Minor code changes and cleanups to get code quality up
12 | * Added docs at [github.io](https://axyr.github.io/ideannotator)
13 | * Added scrutinizer
14 | * Added CodeClimate
15 | * Improved to better match SilverStripe Module standards
16 |
17 |
18 | # 2.0
19 |
20 | * Implemented phpDocumentor
21 | * Remove start/end tags
22 | * All classes in a single file annotated
23 | * Added DataRecord and Data() annotation for _Controller methods
24 |
25 | # 2.0.1 to 2.0.4
26 | * Minor fixes
27 | * Check for Controller::curr(); to support cli
28 | * Correct environment checking
29 | * Added warning messages when a file is not writable or class defenition is misspelled
30 |
31 | # 2.0.5
32 | * Support dot notations in relations
33 |
34 | # 3.0 beta-1
35 | * Support for SilverStripe 4
36 |
37 | # 3.0 rc-1
38 | * Updated support for SilverStripe 4
39 | * Added support for the `through` method
40 |
41 | # 3.0 rc-2
42 | * Fixed bug trimming too much whitespace in rc-1
43 | * Support for short classnames instead of FQN
44 | * ~~require_once in tests no longer needed~~ Nope, still needed
--------------------------------------------------------------------------------
/LICENSE.md:
--------------------------------------------------------------------------------
1 | Copyright (c) 2015-2018, Martijn van Nieuwenhoven/SilverLeague
2 | All rights reserved.
3 |
4 | Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
5 |
6 | 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
7 |
8 | 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
9 |
10 | 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.
11 |
12 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
13 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # silverstripe-ideannotator
2 |
3 | 
4 | [](https://scrutinizer-ci.com/g/silverleague/silverstripe-ideannotator/)
5 | [](https://codecov.io/gh/silverleague/silverstripe-ideannotator)
6 | [](https://packagist.org/packages/silverleague/ideannotator)
7 | [](https://packagist.org/packages/silverleague/ideannotator)
8 | [](https://packagist.org/packages/silverleague/ideannotator)
9 |
10 |
11 | This module generates @property, @method and @mixin tags for DataObjects, PageControllers and (Data)Extensions, so ide's like PHPStorm recognize the database and relations that are set in the $db, $has_one, $has_many and $many_many arrays.
12 |
13 | The docblocks can be generated/updated with each dev/build and with a DataObjectAnnotatorTask per module or classname.
14 |
15 | ## Requirements
16 |
17 | SilverStripe Framework and possible custom code.
18 |
19 | By default, `mysite` and `app` are enabled "modules".
20 |
21 | ### Version ^2:
22 | SilverStripe 3.x framework
23 |
24 | ### Version ^3:
25 | SilverStripe 4.x+
26 |
27 | ## Installation
28 |
29 | ```json
30 | {
31 | "require-dev": {
32 | "silverleague/ideannotator": "3.x-dev"
33 | }
34 | }
35 | ```
36 | Please note, this example omitted any possible modules you require yourself!
37 |
38 | ## Example result
39 |
40 | ```php
41 | 'Varchar(255)',
59 | 'Sort' => 'Int'
60 | );
61 |
62 | private static $has_one = array(
63 | 'Author' => Member::class
64 | );
65 |
66 | private static $has_many = array(
67 | 'Categories' => Category::class
68 | );
69 |
70 | private static $many_many = array(
71 | 'Tags' => Tag::class
72 | );
73 | }
74 | ```
75 |
76 | ## Further information
77 | For installation, see [installation](docs/en/Installation.md)
78 |
79 | For the Code of Conduct, see [CodeOfConduct](docs/en/CodeOfConduct.md)
80 |
81 | For contributing, see [Contributing](CONTRIBUTING.md)
82 |
83 | For further documentation information, see the [docs](docs/en/Index.md)
84 |
85 | ## A word of caution
86 | This module changes the content of your files and currently there is no backup functionality. PHPStorm has a Local history for files and of course you have your code version controlled...
87 | I tried to add complete UnitTests, but I can't garantuee every situation is covered.
88 |
89 | Windows users should be aware that the PHP Docs are generated with PSR in mind and use \n for line endings rather than Window's \r\n, some editors may have a hard time with these line endings.
90 |
91 | This module should **never** be installed on a production environment.
92 |
--------------------------------------------------------------------------------
/_config/config.yml:
--------------------------------------------------------------------------------
1 | ---
2 | Name: ideannotator
3 | ---
4 | SilverStripe\Dev\DevBuildController:
5 | extensions:
6 | - SilverLeague\IDEAnnotator\Extensions\Annotatable
7 | SilverLeague\IDEAnnotator\DataObjectAnnotator:
8 | enabled_modules:
9 | - mysite
10 | - app
11 | dbfield_tagnames:
12 | SilverStripe\ORM\FieldType\DBInt: int
13 | SilverStripe\ORM\FieldType\DBBoolean: bool
14 | SilverStripe\ORM\FieldType\DBFloat: float
15 | SilverStripe\ORM\FieldType\DBDecimal: float
16 |
--------------------------------------------------------------------------------
/codecov.yml:
--------------------------------------------------------------------------------
1 | fixes:
2 | - "ideannotator/src::src"
--------------------------------------------------------------------------------
/composer.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "silverleague/ideannotator",
3 | "description": "Generate PHP DocBlock annotations for DataObject and DataExtension databasefields and relation methods",
4 | "type": "silverstripe-vendormodule",
5 | "keywords": [
6 | "silverstripe",
7 | "database",
8 | "orm",
9 | "docblock",
10 | "ide",
11 | "annotations"
12 | ],
13 | "license": "BSD-3-Clause",
14 | "authors": [
15 | {
16 | "name": "Martijn van Nieuwenhoven",
17 | "email": "info@axyrmedia.nl"
18 | },
19 | {
20 | "name": "Simon 'Firesphere' Erkelens",
21 | "email": "simon@firesphere.dev"
22 | }
23 | ],
24 | "require": {
25 | "php": "^8.0",
26 | "silverstripe/framework": "^4 || ^5",
27 | "phpdocumentor/reflection-docblock": "^5.4"
28 | },
29 | "require-dev": {
30 | "phpunit/phpunit": "^9.5",
31 | "squizlabs/php_codesniffer": "^3.0",
32 | "silverstripe/cms": "^4 || ^5"
33 | },
34 | "autoload": {
35 | "psr-4": {
36 | "SilverLeague\\IDEAnnotator\\": "src/",
37 | "SilverLeague\\IDEAnnotator\\Tests\\": "tests/"
38 | }
39 | },
40 | "extra": {
41 | "branch-alias": {
42 | "3.x-dev": "3.2.x-dev",
43 | "dev-master": "3.x-dev"
44 | }
45 | },
46 | "config": {
47 | "process-timeout": 600,
48 | "allow-plugins": {
49 | "silverstripe/vendor-plugin": true,
50 | "composer/installers": true,
51 | "php-http/discovery": true
52 | }
53 | },
54 | "scripts": {
55 | "lint": "phpcs -s src/",
56 | "lint-clean": "phpcbf src/",
57 | "test": "phpunit -v",
58 | "test-dev": "phpunit -v --filter testAnnotateModule"
59 | },
60 | "prefer-stable": true,
61 | "minimum-stability": "dev"
62 | }
63 |
--------------------------------------------------------------------------------
/docs/en/CodeOfConduct.md:
--------------------------------------------------------------------------------
1 | # Code of Conduct
2 | Please refer to https://docs.silverstripe.org/en/3.3/contributing/code_of_conduct/ for the code of conduct.
3 |
--------------------------------------------------------------------------------
/docs/en/Index.md:
--------------------------------------------------------------------------------
1 | ###[API Documentation](https://axyr.github.io/ideannotator)
2 |
3 | ###[Installation](Installation.md)
4 |
5 | ###[Code of Conduct](CodeOfConduct.md)
6 |
7 | ###[Contributing](../../CONTRIBUTING.md)
8 |
--------------------------------------------------------------------------------
/docs/en/Installation.md:
--------------------------------------------------------------------------------
1 | ## Installation
2 | Either run `composer require silverleague/ideannotator --dev`
3 |
4 | Or add `silverleague/ideannotator: "*"` to `require-dev` in your composer.json file
5 |
6 | Or download and add it to your root directory.
7 |
8 |
9 | ## Config
10 | This module is disabled by default and I recommend to only enable this module in your local development environment, since this module changes the file content of the Dataobject and DataExtension classes.
11 |
12 | You can do this, by using something like this in your mysite/_config.php (not recommended!):
13 |
14 | ```php
15 | if($_SERVER['HTTP_HOST'] == 'mysite.local.dev') {
16 | Config::modify()->set('SilverLeague\IDEAnnotator\DataObjectAnnotator', 'enabled', true);
17 | }
18 | ```
19 |
20 | Even when the module is enabled, the generation will only work in a dev environment. Putting a live site into dev with ?isDev will not alter your files.
21 |
22 | When enabled IdeAnnotator generates the docblocks on dev/build for mysite only.
23 |
24 | You can add extra module folders with the following config setting :
25 |
26 | ```php
27 |
28 | Config::modify()->set('SilverLeague\IDEAnnotator\DataObjectAnnotator', 'enabled_modules', array('mysite', 'otherfolderinsiteroot'));
29 | ```
30 | or
31 | ```yaml
32 |
33 | ---
34 | Only:
35 | environment: 'dev'
36 | ---
37 | SilverLeague\IDEAnnotator\DataObjectAnnotator:
38 | enabled_modules:
39 | - mysite
40 | - otherfolderinsiteroot
41 | ```
42 |
43 | If the module you want annotated, has it's own composer.json file, and a name declared, you can enable it like this:
44 |
45 | ```yaml
46 |
47 | ---
48 | Only:
49 | environment: 'dev'
50 | ---
51 | SilverLeague\IDEAnnotator\DataObjectAnnotator:
52 | enabled_modules:
53 | - mysite
54 | - SilverLeague/IDEAnnotator
55 | ```
56 |
57 | If you don't want to use fully qualified classnames, you can configure that like so:
58 |
59 | ```yaml
60 |
61 | ---
62 | Only:
63 | environment: 'dev'
64 | ---
65 | SilverLeague\IDEAnnotator\DataObjectAnnotator:
66 | enabled: true
67 | use_short_name: true
68 | enabled_modules:
69 | - mysite
70 | ```
71 |
72 | If you want to add extra field types that do not return one of the known values, you can add it as such:
73 |
74 | ```yaml
75 | SilverLeague\IDEAnnotator\DataObjectAnnotator:
76 | dbfield_tagnames:
77 | Symbiote\MultiValueField\ORM\FieldType\MultiValueField: 'MultiValueField|string[]'
78 | ```
79 | **NOTE**
80 |
81 | - Using short names, will also shorten core names like `ManyManyList`, you'll have to adjust your use statements to work.
82 |
83 | - If you change the usage of short names halfway in your project, you may need to clear out all your docblocks before regenerating
84 |
85 | ### Generics
86 |
87 | If you want to enable true generics for DataLists, you can set the `use_generics` parameter to true:
88 |
89 | ```yaml
90 | SilverLeague\IDEAnnotator\DataObjectAnnotator:
91 | enabled: true
92 | use_generics: true
93 | ```
94 |
--------------------------------------------------------------------------------
/phpcs.xml.dist:
--------------------------------------------------------------------------------
1 |
2 |
3 | CodeSniffer ruleset for SilverStripe coding conventions.
4 |
5 | src
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
--------------------------------------------------------------------------------
/phpunit.xml.dist:
--------------------------------------------------------------------------------
1 |
3 |
4 |
5 | tests/
6 |
7 |
8 |
9 | src/
10 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/src/DataObjectAnnotator.php:
--------------------------------------------------------------------------------
1 | '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 |
--------------------------------------------------------------------------------
/src/Extensions/Annotatable.php:
--------------------------------------------------------------------------------
1 | annotateModules();
50 | }
51 |
52 | /**
53 | * Conditionally annotate this project's modules if enabled and not skipped
54 | *
55 | * @return bool Return true if annotation was successful
56 | * @throws NotFoundExceptionInterface
57 | * @throws ReflectionException
58 | */
59 | public function annotateModules()
60 | {
61 | $envIsAllowed = Director::isDev() && DataObjectAnnotator::config()->get('enabled');
62 | $skipAnnotation = $this->owner->getRequest()->getVar('skipannotation');
63 |
64 | // Only instatiate things when we want to run it, this is for when the module is accidentally installed
65 | // on non-dev environments for example
66 | if ($skipAnnotation === null && $envIsAllowed) {
67 | $this->setUp();
68 |
69 | $this->displayMessage('Generating class docblocks', true, false);
70 |
71 | $modules = $this->permissionChecker->enabledModules();
72 | foreach ($modules as $module) {
73 | $this->annotator->annotateModule($module);
74 | }
75 |
76 | $this->displayMessage('Docblock generation finished!', true, true);
77 |
78 | return true;
79 | }
80 |
81 | return false;
82 | }
83 |
84 | /**
85 | * Annotatable setup.
86 | * This is theoretically a constructor, but to save memory we're using setup
87 | * called from {@see afterCallActionHandler}
88 | * @throws NotFoundExceptionInterface
89 | */
90 | public function setUp()
91 | {
92 | $this->annotator = Injector::inst()->get(DataObjectAnnotator::class);
93 | $this->permissionChecker = Injector::inst()->get(AnnotatePermissionChecker::class);
94 | }
95 |
96 | /**
97 | * @param string $message
98 | * @param bool $heading
99 | * @param bool $end
100 | */
101 | public function displayMessage($message, $heading = false, $end = false)
102 | {
103 | if ($heading) {
104 | if (!$end) {
105 | echo Director::is_cli() ?
106 | strtoupper("\n$message\n\n") :
107 | "
$message
";
108 | } else {
109 | echo Director::is_cli() ? strtoupper("\n" . $message) : "
$message
";
110 | }
111 | } else {
112 | echo Director::is_cli() ? "\n$message\n\n" : "$message";
113 | }
114 | }
115 |
116 | /**
117 | * @return DataObjectAnnotator
118 | */
119 | public function getAnnotator()
120 | {
121 | return $this->annotator;
122 | }
123 |
124 | /**
125 | * @return AnnotatePermissionChecker
126 | */
127 | public function getPermissionChecker()
128 | {
129 | return $this->permissionChecker;
130 | }
131 | }
132 |
--------------------------------------------------------------------------------
/src/Generators/AbstractTagGenerator.php:
--------------------------------------------------------------------------------
1 | getSupportedTagTypes();
61 | * @var Tag[]
62 | */
63 | protected $tags = [];
64 |
65 | protected static $pageClassesCache = [];
66 |
67 | /**
68 | * DocBlockTagGenerator constructor.
69 | *
70 | * @param string $className
71 | * @param $existingTags
72 | * @throws ReflectionException
73 | */
74 | public function __construct($className, $existingTags)
75 | {
76 | $this->className = $className;
77 | $this->existingTags = (array)$existingTags;
78 | $this->reflector = new ReflectionClass($className);
79 | $this->tags = $this->getSupportedTagTypes();
80 |
81 | //Init the tag factory
82 | if (DataObjectAnnotator::config()->get('use_short_name')) {
83 | $this->docBlockFactory = $this->createShortNameFactory();
84 | } else {
85 | $this->docBlockFactory = DocBlockFactory::createInstance();
86 | }
87 |
88 | $this->generateTags();
89 | }
90 |
91 | /**
92 | * List of supported tags.
93 | *
94 | * Each tag type can hold many tags, so we keep them grouped.
95 | * Also used to reset the tag list after each run
96 | *
97 | * @return array
98 | */
99 | public function getSupportedTagTypes()
100 | {
101 | return [
102 | 'properties' => [],
103 | 'methods' => [],
104 | 'mixins' => [],
105 | 'other' => []
106 | ];
107 | }
108 |
109 | /**
110 | * @return void
111 | */
112 | abstract protected function generateTags();
113 |
114 | /**
115 | * @return Tag[]
116 | */
117 | public function getTags()
118 | {
119 | return (array)call_user_func_array('array_merge', array_values($this->tags));
120 | }
121 |
122 | /**
123 | * Generate the mixins for DataExtensions.
124 | */
125 | protected function generateExtensionsTags()
126 | {
127 | if ($fields = (array)$this->getClassConfig('extensions')) {
128 | foreach ($fields as $fieldName) {
129 | $mixinName = $this->getAnnotationClassName($fieldName);
130 | $this->pushMixinTag($mixinName);
131 | }
132 | }
133 | if (is_subclass_of($this->className, DataObject::class)) {
134 | $baseFields = Config::inst()->get(DataObject::class, 'extensions', Config::UNINHERITED);
135 | if ($baseFields) {
136 | foreach ($baseFields as $fieldName) {
137 | $mixinName = $this->getAnnotationClassName($fieldName);
138 | $this->pushMixinTag($mixinName);
139 | }
140 | }
141 | }
142 | }
143 |
144 | /**
145 | * @param string $key
146 | * @return mixed
147 | */
148 | protected function getClassConfig($key)
149 | {
150 | return Config::inst()->get($this->className, $key, Config::UNINHERITED);
151 | }
152 |
153 | /**
154 | * Check if we need to use the short name for a class
155 | *
156 | * @param string $class
157 | * @return string
158 | */
159 | protected function getAnnotationClassName($class)
160 | {
161 | [$class] = explode('.', $class); // Remove dot-notated extension parts
162 | if (DataObjectAnnotator::config()->get('use_short_name')) {
163 | return ClassInfo::shortName($class);
164 | }
165 |
166 | return "\\$class";
167 | }
168 |
169 | /**
170 | * @param $tagString
171 | */
172 | protected function pushMixinTag($tagString)
173 | {
174 | $this->tags['mixins'][$tagString] = $this->pushTagWithExistingComment('mixin', $tagString);
175 | }
176 |
177 | /**
178 | * @param $type
179 | * @param $tagString
180 | * @return Tag
181 | */
182 | protected function pushTagWithExistingComment($type, $tagString)
183 | {
184 | $tagString = sprintf('@%s %s', $type, $tagString);
185 | $tagString .= $this->getExistingTagCommentByTagString($tagString);
186 |
187 | $tmpBlock = $this->docBlockFactory->create("/**\n* " . $tagString . "\n*/");
188 | return $tmpBlock->getTagsByName($type)[0];
189 | }
190 |
191 | /**
192 | * @param string $tagString
193 | * @return string
194 | */
195 | public function getExistingTagCommentByTagString($tagString)
196 | {
197 | foreach ($this->getExistingTags() as $tag) {
198 | $content = $tag->__toString();
199 | // A tag should be followed by a space before it's description
200 | // This is to prevent `TestThing` and `Test` to be seen as the same, when the shorter
201 | // is after the longer name
202 | if (strpos($content, $tagString . ' ') !== false) {
203 | return str_replace($tagString, '', $content);
204 | }
205 | }
206 |
207 | return '';
208 | }
209 |
210 | /**
211 | * @return Tag[]
212 | */
213 | public function getExistingTags()
214 | {
215 | return $this->existingTags;
216 | }
217 |
218 | /**
219 | * Generate the Owner-properties for extensions.
220 | *
221 | * @throws ReflectionException
222 | */
223 | protected function generateOwnerTags()
224 | {
225 | $className = $this->className;
226 | // If className is abstract, Injector will fail to instantiate it
227 | $reflection = new ReflectionClass($className);
228 | if ($reflection->isAbstract()) {
229 | return;
230 | }
231 | if ($reflection->isSubclassOf(Extension::class)) {
232 | $owners = iterator_to_array($this->getOwnerClasses($className));
233 | $owners[] = $this->className;
234 | $tagString = sprintf('\\%s $owner', implode("|\\", array_values($owners)));
235 | if (DataObjectAnnotator::config()->get('use_short_name')) {
236 | foreach ($owners as $key => $owner) {
237 | $owners[$key] = $this->getAnnotationClassName($owner);
238 | }
239 | $tagString = implode("|", array_values($owners)) . ' $owner';
240 | }
241 | $this->pushPropertyTag($tagString);
242 | }
243 | }
244 |
245 | /**
246 | * Get all owner classes of the given extension class
247 | *
248 | * @param string $extensionClass Class name of the extension
249 | * @return string[]|Generator List of all direct owners of this extension
250 | */
251 | protected function getOwnerClasses($extensionClass)
252 | {
253 | foreach (DataObjectAnnotator::getExtensionClasses() as $objectClass) {
254 | $config = Config::inst()->get(
255 | $objectClass,
256 | 'extensions',
257 | Config::UNINHERITED | Config::EXCLUDE_EXTRA_SOURCES
258 | ) ?: [];
259 | foreach ($config as $candidateClass) {
260 | if (Extension::get_classname_without_arguments($candidateClass) === $extensionClass) {
261 | yield $objectClass;
262 | break;
263 | }
264 | }
265 | }
266 | }
267 |
268 | /**
269 | * @param string $tagString
270 | */
271 | protected function pushPropertyTag($tagString)
272 | {
273 | $this->tags['properties'][$tagString] = $this->pushTagWithExistingComment('property', $tagString);
274 | }
275 |
276 | /**
277 | * @param string $methodName
278 | * @param string $tagString
279 | */
280 | protected function pushMethodTag($methodName, $tagString)
281 | {
282 | // Exception for `data()` method is needed
283 | if (!$this->reflector->hasMethod($methodName) || $methodName === 'data()') {
284 | $this->tags['methods'][$tagString] = $this->pushTagWithExistingComment('method', $tagString);
285 | }
286 | }
287 |
288 | /**
289 | * Factory method for easy instantiation.
290 | * @param array|Factory> $additionalTags
291 | * @return DocBlockFactoryInterface
292 | */
293 | protected function createShortNameFactory(array $additionalTags = []): DocBlockFactory
294 | {
295 | $fqsenResolver = new ShortNameResolver();
296 | $tagFactory = new StandardTagFactory($fqsenResolver);
297 | $descriptionFactory = new DescriptionFactory($tagFactory);
298 | $typeResolver = new TypeResolver($fqsenResolver);
299 |
300 | $phpstanTagFactory = new AbstractPHPStanFactory(
301 | new ParamFactory($typeResolver, $descriptionFactory),
302 | new VarFactory($typeResolver, $descriptionFactory),
303 | new ReturnFactory($typeResolver, $descriptionFactory),
304 | new PropertyFactory($typeResolver, $descriptionFactory),
305 | new PropertyReadFactory($typeResolver, $descriptionFactory),
306 | new PropertyWriteFactory($typeResolver, $descriptionFactory),
307 | new MethodFactory($typeResolver, $descriptionFactory)
308 | );
309 |
310 | $tagFactory->addService($descriptionFactory);
311 | $tagFactory->addService($typeResolver);
312 | $tagFactory->registerTagHandler('param', $phpstanTagFactory);
313 | $tagFactory->registerTagHandler('var', $phpstanTagFactory);
314 | $tagFactory->registerTagHandler('return', $phpstanTagFactory);
315 | $tagFactory->registerTagHandler('property', $phpstanTagFactory);
316 | $tagFactory->registerTagHandler('property-read', $phpstanTagFactory);
317 | $tagFactory->registerTagHandler('property-write', $phpstanTagFactory);
318 | $tagFactory->registerTagHandler('method', $phpstanTagFactory);
319 |
320 | $docBlockFactory = new DocBlockFactory($descriptionFactory, $tagFactory);
321 | foreach ($additionalTags as $tagName => $tagHandler) {
322 | $docBlockFactory->registerTagHandler($tagName, $tagHandler);
323 | }
324 |
325 | return $docBlockFactory;
326 | }
327 | }
328 |
--------------------------------------------------------------------------------
/src/Generators/ControllerTagGenerator.php:
--------------------------------------------------------------------------------
1 | mapPageTypesToControllerName();
23 |
24 | parent::__construct($className, $existingTags);
25 | }
26 |
27 | /**
28 | * @return void
29 | * @throws ReflectionException
30 | */
31 | protected function generateTags()
32 | {
33 | $this->generateControllerObjectTags();
34 | $this->generateExtensionsTags();
35 | $this->generateOwnerTags();
36 | }
37 |
38 | /**
39 | * Generate the controller tags, these differ slightly from the standard ORM tags
40 | *
41 | * @throws ReflectionException
42 | */
43 | protected function generateControllerObjectTags()
44 | {
45 | $shortName = ClassInfo::shortName($this->className);
46 | // Strip "Controller" or "_Controller" from the class short name
47 | $shortSansController = str_replace(['_Controller', 'Controller'], '', $shortName);
48 | // And push it back in :)
49 | $pageClassname = str_replace($shortName, $shortSansController, $this->className);
50 |
51 | if (class_exists($pageClassname) && $this->isContentController($this->className)) {
52 | $pageClassname = $this->getAnnotationClassName($pageClassname);
53 |
54 | $this->pushPropertyTag(sprintf('%s $dataRecord', $pageClassname));
55 | $this->pushMethodTag('data()', sprintf('%s data()', $pageClassname));
56 |
57 | // don't mixin Page, since this is a ContentController method
58 | if ($pageClassname !== 'Page') {
59 | $this->pushMixinTag($pageClassname);
60 | }
61 | } elseif ($this->isContentController($this->className) && array_key_exists($this->className, self::$pageClassesCache)) {
62 | $pageClassname = $this->getAnnotationClassName(self::$pageClassesCache[$this->className]);
63 |
64 | $this->pushPropertyTag(sprintf('%s $dataRecord', $pageClassname));
65 | $this->pushMethodTag('data()', sprintf('%s data()', $pageClassname));
66 |
67 | // don't mixin Page, since this is a ContentController method
68 | if ($pageClassname !== 'Page') {
69 | $this->pushMixinTag($pageClassname);
70 | }
71 | }
72 | }
73 |
74 | /**
75 | * @param string $className
76 | * @return bool
77 | * @throws ReflectionException
78 | */
79 | protected function isContentController($className)
80 | {
81 | $reflector = new ReflectionClass($className);
82 |
83 | return ClassInfo::exists(ContentController::class)
84 | && $reflector->isSubclassOf(ContentController::class);
85 | }
86 |
87 | /**
88 | * Generates the cache of Page types to Controllers when the controller_name config is used
89 | */
90 | protected function mapPageTypesToControllerName()
91 | {
92 | if (empty(self::$pageClassesCache)) {
93 | $pageClasses = ClassInfo::subclassesFor(Page::class);
94 | foreach ($pageClasses as $pageClassname) {
95 | $controllerName = Config::inst()->get($pageClassname, 'controller_name');
96 | if (!empty($controllerName)) {
97 | self::$pageClassesCache[$controllerName] = $pageClassname;
98 | }
99 | }
100 | }
101 | }
102 | }
103 |
--------------------------------------------------------------------------------
/src/Generators/DocBlockGenerator.php:
--------------------------------------------------------------------------------
1 | className = $className;
58 | $this->reflector = new ReflectionClass($className);
59 | $this->docBlockFactory = DocBlockFactory::createInstance();
60 |
61 | $generatorClass = $this->reflector->isSubclassOf(Controller::class)
62 | ? ControllerTagGenerator::class : OrmTagGenerator::class;
63 |
64 | $this->tagGenerator = new $generatorClass($className, $this->getExistingTags());
65 | }
66 |
67 | /**
68 | * @return Tag[]
69 | * @throws InvalidArgumentException
70 | */
71 | public function getExistingTags()
72 | {
73 | $docBlock = $this->getExistingDocBlock();
74 | if (!$docBlock) {
75 | return [];
76 | }
77 |
78 | $docBlock = $this->docBlockFactory->create($docBlock);
79 |
80 | return $docBlock->getTags();
81 | }
82 |
83 | /**
84 | * Not that in case there are multiple doblocks for a class,
85 | * the last one will be returned
86 | *
87 | * If we file old style generated docblocks we remove them
88 | *
89 | * @return bool|string
90 | */
91 | public function getExistingDocBlock()
92 | {
93 | return $this->reflector->getDocComment();
94 | }
95 |
96 | /**
97 | * @return DocBlock|string
98 | * @throws LogicException
99 | * @throws InvalidArgumentException
100 | */
101 | public function getGeneratedDocBlock()
102 | {
103 | $docBlock = $this->getExistingDocBlock();
104 |
105 | return $this->mergeGeneratedTagsIntoDocBlock($docBlock);
106 | }
107 |
108 | /**
109 | * @param string $existingDocBlock
110 | * @return string
111 | * @throws LogicException
112 | * @throws InvalidArgumentException
113 | */
114 | protected function mergeGeneratedTagsIntoDocBlock($existingDocBlock)
115 | {
116 | $docBlock = $this->docBlockFactory->create(($existingDocBlock ?: "/**\n*/"));
117 |
118 | $summary = $docBlock->getSummary();
119 | if (!$summary) {
120 | $summary = sprintf('Class \\%s', $this->className);
121 | }
122 |
123 | $generatedTags = $this->getGeneratedTags();
124 | $mergedTags = [];
125 | foreach ($generatedTags as $generatedTag) {
126 | $currentTag = $docBlock->getTagsByName($generatedTag->getName())[0] ?? null;
127 |
128 | // If there is an existing tag with the same name, preserve its description
129 | // There is no setDescription method so we use reflection
130 | if ($currentTag && $currentTag instanceof BaseTag && $currentTag->getDescription()) {
131 | $refObject = new ReflectionObject($generatedTag);
132 | $refProperty = $refObject->getProperty('description');
133 | $refProperty->setAccessible(true);
134 | $refProperty->setValue($generatedTag, $currentTag->getDescription());
135 | }
136 | $mergedTags[] = $generatedTag;
137 | }
138 | foreach ($docBlock->getTags() as $existingTag) {
139 | // Skip any property, method or mixin tag
140 | if ($existingTag instanceof Property || $existingTag instanceof Method || $existingTag instanceof Mixin) {
141 | continue;
142 | }
143 | $mergedTags[] = $existingTag;
144 | }
145 |
146 | $docBlock = new DocBlock($summary, $docBlock->getDescription(), $mergedTags);
147 |
148 | $serializer = new Serializer();
149 | $docBlock = $serializer->getDocComment($docBlock);
150 |
151 | return $docBlock;
152 | }
153 |
154 | /**
155 | * Remove all existing tags that are supported by this module.
156 | *
157 | * This will make sure that removed ORM properties and Extenions will not remain in the docblock,
158 | * while providing the option to manually add docblocks like @author etc.
159 | *
160 | * @param $docBlock
161 | * @return string
162 | */
163 | public function removeExistingSupportedTags($docBlock)
164 | {
165 | $replacements = [
166 | "/ \* @property ([\s\S]*?)\n/",
167 | "/ \* @method ([\s\S]*?)\n/",
168 | "/ \* @mixin ([\s\S]*?)\n/"
169 | ];
170 |
171 | return (string)preg_replace($replacements, '', $docBlock);
172 | }
173 |
174 | /**
175 | * @return Tag[]
176 | */
177 | public function getGeneratedTags()
178 | {
179 | return $this->tagGenerator->getTags();
180 | }
181 | }
182 |
--------------------------------------------------------------------------------
/src/Generators/OrmTagGenerator.php:
--------------------------------------------------------------------------------
1 | {$function}();
43 | }
44 | }
45 |
46 | /**
47 | * Generate the $db property values.
48 | */
49 | protected function generateDBTags()
50 | {
51 | if ($fields = (array)$this->getClassConfig('db')) {
52 | foreach ($fields as $fieldName => $dbFieldType) {
53 | $this->pushPropertyTag($this->getTagNameForDBField($dbFieldType) . " \$$fieldName");
54 | }
55 | }
56 | }
57 |
58 | /**
59 | * @param string $dbFieldType
60 | * @return string
61 | */
62 | public function getTagNameForDBField($dbFieldType)
63 | {
64 | // some fields in 3rd-party modules require a name...
65 | $fieldObj = Injector::inst()->create($dbFieldType, 'DummyName');
66 |
67 | $fieldNames = DataObjectAnnotator::config()->get('dbfield_tagnames');
68 |
69 | foreach ($fieldNames as $dbClass => $tagName) {
70 | if (class_exists($dbClass)) {
71 | $obj = Injector::inst()->create($dbClass);
72 | if ($fieldObj instanceof $obj) {
73 | return $tagName;
74 | }
75 | }
76 | }
77 |
78 | return self::defaultType();
79 | }
80 |
81 | public static function defaultType()
82 | {
83 | $type = 'string';
84 | if (version_compare(PHP_VERSION, '8.0.0') >= 0) {
85 | $type = '?string';
86 | }
87 | return $type;
88 | }
89 |
90 | /**
91 | * Generate the $belongs_to property values.
92 | */
93 | protected function generateBelongsToTags()
94 | {
95 | if ($fields = (array)$this->getClassConfig('belongs_to')) {
96 | foreach ($fields as $fieldName => $dataObjectName) {
97 | $dataObjectName = $this->resolveDotNotation($dataObjectName);
98 | $dataObjectName = $this->getAnnotationClassName($dataObjectName);
99 | $tagString = "{$dataObjectName} {$fieldName}()";
100 |
101 | $this->pushMethodTag($fieldName, $tagString);
102 | }
103 | }
104 | }
105 |
106 | /**
107 | * @param $dataObjectName
108 | * @return mixed
109 | */
110 | protected function resolveDotNotation($dataObjectName)
111 | {
112 | list($dataObjectName) = explode('.', $dataObjectName, 2);
113 |
114 | return $dataObjectName;
115 | }
116 |
117 | /**
118 | * Generate the $has_one property and method values.
119 | */
120 | protected function generateHasOneTags()
121 | {
122 | if ($fields = (array)$this->getClassConfig('has_one')) {
123 | foreach ($fields as $fieldName => $dataObjectName) {
124 | $this->pushPropertyTag("int \${$fieldName}ID");
125 |
126 | if ($dataObjectName === DataObject::class) {
127 | $this->pushPropertyTag("string \${$fieldName}Class");
128 | }
129 |
130 | $dataObjectName = $this->getAnnotationClassName($dataObjectName);
131 | $tagString = "{$dataObjectName} {$fieldName}()";
132 |
133 | $this->pushMethodTag($fieldName, $tagString);
134 | }
135 | }
136 | }
137 |
138 | /**
139 | * Generate the $has_many method values.
140 | */
141 | protected function generateHasManyTags()
142 | {
143 | $this->generateTagsForDataLists($this->getClassConfig('has_many'), DataList::class);
144 | }
145 |
146 | /**
147 | * @param array $fields
148 | * @param string $listType
149 | */
150 | protected function generateTagsForDataLists($fields, $listType = DataList::class)
151 | {
152 | $useGenerics = DataObjectAnnotator::config()->get('use_generics');
153 |
154 | if (!empty($fields)) {
155 | foreach ((array)$fields as $fieldName => $dataObjectName) {
156 | $fieldName = trim($fieldName);
157 | // A many_many with a relation through another DataObject
158 | if (is_array($dataObjectName)) {
159 | $dataObjectName = $dataObjectName['through'];
160 | }
161 | $dataObjectName = $this->resolveDotNotation($dataObjectName);
162 | $listName = $this->getAnnotationClassName($listType);
163 | $dataObjectName = $this->getAnnotationClassName($dataObjectName);
164 |
165 | $tagString = "{$listName}|{$dataObjectName}[] {$fieldName}()";
166 | if ($useGenerics) {
167 | $tagString = "{$listName}<$dataObjectName> {$fieldName}()";
168 | }
169 | $this->pushMethodTag($fieldName, $tagString);
170 | }
171 | }
172 | }
173 |
174 | /**
175 | * Generate the $many_many method values.
176 | */
177 | protected function generateManyManyTags()
178 | {
179 | $this->generateTagsForDataLists($this->getClassConfig('many_many'), ManyManyList::class);
180 | }
181 |
182 | /**
183 | * Generate the $belongs_many_many method values.
184 | */
185 | protected function generateBelongsManyManyTags()
186 | {
187 | $this->generateTagsForDataLists($this->getClassConfig('belongs_many_many'), ManyManyList::class);
188 | }
189 | }
190 |
--------------------------------------------------------------------------------
/src/Helpers/AnnotateClassInfo.php:
--------------------------------------------------------------------------------
1 | className = $className;
36 |
37 | $this->reflector = new ReflectionClass($className);
38 | }
39 |
40 | /**
41 | * Where module name is a folder in the webroot.
42 | *
43 | * @return string
44 | */
45 | public function getModuleName()
46 | {
47 | /** @var ModuleManifest $moduleManifest */
48 | $moduleManifest = Injector::inst()->createWithArgs(ModuleManifest::class, [Director::baseFolder()]);
49 | $module = $moduleManifest->getModuleByPath($this->reflector->getFileName());
50 |
51 | return $module->getName();
52 | }
53 |
54 | /**
55 | * @return string
56 | */
57 | public function getClassFilePath()
58 | {
59 | return $this->reflector->getFileName();
60 | }
61 | }
62 |
--------------------------------------------------------------------------------
/src/Helpers/AnnotatePermissionChecker.php:
--------------------------------------------------------------------------------
1 | isEnabled()) {
45 | return false;
46 | }
47 |
48 | return Director::isDev();
49 | }
50 |
51 | /**
52 | * @return bool
53 | */
54 | public function isEnabled()
55 | {
56 | return (bool)DataObjectAnnotator::config()->get('enabled');
57 | }
58 |
59 | /**
60 | * @return array
61 | */
62 | public function getSupportedParentClasses()
63 | {
64 | return $this->supportedParentClasses;
65 | }
66 |
67 | /**
68 | * Check if a DataObject or DataExtension subclass is allowed by checking if the file
69 | * is in the $allowed_modules array
70 | * The permission is checked by matching the filePath and modulePath
71 | *
72 | * @param $className
73 | *
74 | * @return bool
75 | * @throws NotFoundExceptionInterface
76 | * @throws ReflectionException
77 | */
78 | public function classNameIsAllowed($className)
79 | {
80 | if ($this->classNameIsSupported($className)) {
81 | $classInfo = new AnnotateClassInfo($className);
82 | $filePath = $classInfo->getClassFilePath();
83 | $module = Injector::inst()->createWithArgs(
84 | ModuleManifest::class,
85 | [Director::baseFolder()]
86 | )->getModuleByPath($filePath);
87 |
88 | $allowedModules = (array)DataObjectAnnotator::config()->get('enabled_modules');
89 |
90 | return in_array($module->getName(), $allowedModules, true);
91 | }
92 |
93 | return false;
94 | }
95 |
96 | /**
97 | * Check if a (subclass of ) class is a supported
98 | *
99 | * @param $className
100 | * @return bool
101 | */
102 | public function classNameIsSupported($className)
103 | {
104 | foreach ($this->supportedParentClasses as $supportedParent) {
105 | if (is_subclass_of($className, $supportedParent)) {
106 | return true;
107 | }
108 | }
109 |
110 | return false;
111 | }
112 |
113 | /**
114 | * Check if a module is in the $allowed_modules array
115 | * Required for the buildTask.
116 | *
117 | * @param string $moduleName
118 | *
119 | * @return bool
120 | */
121 | public function moduleIsAllowed($moduleName)
122 | {
123 | return in_array($moduleName, $this->enabledModules(), false);
124 | }
125 |
126 | /**
127 | * @return array
128 | */
129 | public function enabledModules()
130 | {
131 | $enabled = (array)DataObjectAnnotator::config()->get('enabled_modules');
132 |
133 | // modules might be enabled more then once.
134 | return array_combine($enabled, $enabled);
135 | }
136 | }
137 |
--------------------------------------------------------------------------------
/src/Reflection/ShortNameResolver.php:
--------------------------------------------------------------------------------
1 | title = 'DataObject annotations for specific DataObjects, Extensions or Controllers';
31 |
32 | $this->description = 'DataObject Annotator annotates your DO\'s if possible,' .
33 | ' helping you write better code.' .
34 | '
Usage: add the module or DataObject as parameter to the URL,' .
35 | ' e.g. ?module=mysite';
36 | }
37 |
38 | /**
39 | * @param HTTPRequest $request
40 | * @return bool
41 | * @throws ReflectionException
42 | * @throws NotFoundExceptionInterface
43 | */
44 | public function run($request)
45 | {
46 | /* @var $permissionChecker AnnotatePermissionChecker */
47 | $permissionChecker = Injector::inst()->get(AnnotatePermissionChecker::class);
48 |
49 | if (!$permissionChecker->environmentIsAllowed()) {
50 | return false;
51 | }
52 |
53 | /* @var $annotator DataObjectAnnotator */
54 | $annotator = DataObjectAnnotator::create();
55 |
56 | $annotator->annotateObject($request->getVar('object'));
57 |
58 | $annotator->annotateModule($request->getVar('module'));
59 |
60 | return true;
61 | }
62 | }
63 |
--------------------------------------------------------------------------------