├── .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 | ![Build Status](https://github.com/silverleague/silverstripe-ideannotator/actions/workflows/ci.yml/badge.svg) 4 | [![Scrutinizer](https://img.shields.io/scrutinizer/g/silverleague/silverstripe-ideannotator.svg)](https://scrutinizer-ci.com/g/silverleague/silverstripe-ideannotator/) 5 | [![codecov](https://codecov.io/gh/silverleague/silverstripe-ideannotator/branch/master/graph/badge.svg)](https://codecov.io/gh/silverleague/silverstripe-ideannotator) 6 | [![Packagist](https://img.shields.io/packagist/dt/silverleague/ideannotator.svg)](https://packagist.org/packages/silverleague/ideannotator) 7 | [![Packagist](https://img.shields.io/packagist/v/silverleague/ideannotator.svg)](https://packagist.org/packages/silverleague/ideannotator) 8 | [![Packagist Pre Release](https://img.shields.io/packagist/vpre/silverleague/ideannotator.svg)](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

$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 | --------------------------------------------------------------------------------