├── codecov.yml ├── .gitattributes ├── docs └── en │ ├── CodeOfConduct.md │ ├── Index.md │ └── Installation.md ├── phpunit.xml.dist ├── .github └── workflows │ └── ci.yml ├── .gitignore ├── .scrutinizer.yml ├── .codeclimate.yml ├── _config └── config.yml ├── .editorconfig ├── src ├── Reflection │ └── ShortNameResolver.php ├── Helpers │ ├── AnnotateClassInfo.php │ └── AnnotatePermissionChecker.php ├── Tasks │ └── DataObjectAnnotatorTask.php ├── Generators │ ├── ControllerTagGenerator.php │ ├── DocBlockGenerator.php │ ├── OrmTagGenerator.php │ └── AbstractTagGenerator.php ├── Extensions │ └── Annotatable.php └── DataObjectAnnotator.php ├── Changelog.md ├── CONTRIBUTING.md ├── LICENSE.md ├── phpcs.xml.dist ├── composer.json └── README.md /codecov.yml: -------------------------------------------------------------------------------- 1 | fixes: 2 | - "ideannotator/src::src" -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | /tests export-ignore 2 | /.travis.yml export-ignore 3 | /phpunit.xml export-ignore 4 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /phpunit.xml.dist: -------------------------------------------------------------------------------- 1 | 2 | 3 | tests/unit/ 4 | 5 | 6 | -------------------------------------------------------------------------------- /.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@v2 12 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /_config/config.yml: -------------------------------------------------------------------------------- 1 | --- 2 | Name: ideannotator 3 | --- 4 | SilverStripe\Dev\Command\DbBuild: 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 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /src/Reflection/ShortNameResolver.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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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": "^6", 27 | "phpdocumentor/reflection-docblock": "^5.4" 28 | }, 29 | "require-dev": { 30 | "phpunit/phpunit": "^11.3", 31 | "squizlabs/php_codesniffer": "^3.0", 32 | "silverstripe/cms": "^6" 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 | -------------------------------------------------------------------------------- /src/Tasks/DataObjectAnnotatorTask.php: -------------------------------------------------------------------------------- 1 | get(AnnotatePermissionChecker::class); 33 | 34 | if (!$permissionChecker->environmentIsAllowed()) { 35 | return Command::FAILURE; 36 | } 37 | 38 | /* @var $annotator DataObjectAnnotator */ 39 | $annotator = DataObjectAnnotator::create(); 40 | $module = $input->hasOption('module') ? $input->getOption('module') : null; 41 | $object = $input->hasOption('object') ? $input->getOption('object') : null; 42 | 43 | $annotator->annotateObject($object); 44 | 45 | $annotator->annotateModule($module); 46 | 47 | return Command::SUCCESS; 48 | } 49 | 50 | public function getOptions(): array 51 | { 52 | return [ 53 | new InputOption('module', null, InputOption::VALUE_OPTIONAL, 'Annotate a specific module'), 54 | new InputOption('object', null, InputOption::VALUE_OPTIONAL, 'Annotate a specific class'), 55 | ]; 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /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 | 95 | ### Disabling Annotations on Dev/Build 96 | 97 | If you need to disable annotations on dev/build you can use the `annotate_on_build` parameter: 98 | 99 | ```yaml 100 | SilverStripe\Dev\DevBuildController: 101 | annotate_on_build: false 102 | ``` 103 | -------------------------------------------------------------------------------- /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, 5,x 26 | 27 | ### Version ^4: 28 | Silverstripe 6.x+ 29 | 30 | ## Installation 31 | 32 | ```json 33 | { 34 | "require-dev": { 35 | "silverleague/ideannotator": "^4" 36 | } 37 | } 38 | ``` 39 | Please note, this example omitted any possible modules you require yourself! 40 | 41 | ## Example result 42 | 43 | ```php 44 | 'Varchar(255)', 62 | 'Sort' => 'Int' 63 | ); 64 | 65 | private static $has_one = array( 66 | 'Author' => Member::class 67 | ); 68 | 69 | private static $has_many = array( 70 | 'Categories' => Category::class 71 | ); 72 | 73 | private static $many_many = array( 74 | 'Tags' => Tag::class 75 | ); 76 | } 77 | ``` 78 | 79 | ## Further information 80 | For installation, see [installation](docs/en/Installation.md) 81 | 82 | For the Code of Conduct, see [CodeOfConduct](docs/en/CodeOfConduct.md) 83 | 84 | For contributing, see [Contributing](CONTRIBUTING.md) 85 | 86 | For further documentation information, see the [docs](docs/en/Index.md) 87 | 88 | ## A word of caution 89 | 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... 90 | I tried to add complete UnitTests, but I can't garantuee every situation is covered. 91 | 92 | 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. 93 | 94 | This module should **never** be installed on a production environment. 95 | -------------------------------------------------------------------------------- /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/Extensions/Annotatable.php: -------------------------------------------------------------------------------- 1 | owner->config()->annotate_on_build) { 57 | $this->annotateModules($output); 58 | } 59 | } 60 | 61 | /** 62 | * Conditionally annotate this project's modules if enabled and not skipped 63 | * @param PolyOutput $output Output pipe for the task 64 | * @return bool Return true if annotation was successful 65 | * @throws NotFoundExceptionInterface 66 | * @throws ReflectionException 67 | */ 68 | public function annotateModules(PolyOutput $output) 69 | { 70 | $envIsAllowed = Director::isDev() && DataObjectAnnotator::config()->get('enabled'); 71 | 72 | // Only instatiate things when we want to run it, this is for when the module is accidentally installed 73 | // on non-dev environments for example 74 | if ($envIsAllowed) { 75 | $this->setUp(); 76 | 77 | $output->writeln(['Generating class docblocks', '']); 78 | $output->startList(PolyOutput::LIST_UNORDERED); 79 | 80 | $modules = $this->permissionChecker->enabledModules(); 81 | foreach ($modules as $module) { 82 | $this->annotator->annotateModule($module); 83 | } 84 | 85 | $output->stopList(); 86 | 87 | $output->writeln(['Docblock generation finished!', '']); 88 | 89 | return true; 90 | } 91 | 92 | return false; 93 | } 94 | 95 | /** 96 | * Annotatable setup. 97 | * This is theoretically a constructor, but to save memory we're using setup 98 | * called from {@see onAfterBuild} 99 | * @throws NotFoundExceptionInterface 100 | */ 101 | public function setUp() 102 | { 103 | $this->annotator = Injector::inst()->get(DataObjectAnnotator::class); 104 | $this->permissionChecker = Injector::inst()->get(AnnotatePermissionChecker::class); 105 | } 106 | 107 | /** 108 | * @return DataObjectAnnotator 109 | */ 110 | public function getAnnotator() 111 | { 112 | return $this->annotator; 113 | } 114 | 115 | /** 116 | * @return AnnotatePermissionChecker 117 | */ 118 | public function getPermissionChecker() 119 | { 120 | return $this->permissionChecker; 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /src/Helpers/AnnotatePermissionChecker.php: -------------------------------------------------------------------------------- 1 | isEnabled()) { 44 | return false; 45 | } 46 | 47 | return Director::isDev(); 48 | } 49 | 50 | /** 51 | * @return bool 52 | */ 53 | public function isEnabled() 54 | { 55 | return (bool)DataObjectAnnotator::config()->get('enabled'); 56 | } 57 | 58 | /** 59 | * @return array 60 | */ 61 | public function getSupportedParentClasses() 62 | { 63 | return $this->supportedParentClasses; 64 | } 65 | 66 | /** 67 | * Check if a DataObject or DataExtension subclass is allowed by checking if the file 68 | * is in the $allowed_modules array 69 | * The permission is checked by matching the filePath and modulePath 70 | * 71 | * @param $className 72 | * 73 | * @return bool 74 | * @throws NotFoundExceptionInterface 75 | * @throws ReflectionException 76 | */ 77 | public function classNameIsAllowed($className) 78 | { 79 | if ($this->classNameIsSupported($className)) { 80 | $classInfo = new AnnotateClassInfo($className); 81 | $filePath = $classInfo->getClassFilePath(); 82 | $module = Injector::inst()->createWithArgs( 83 | ModuleManifest::class, 84 | [Director::baseFolder()] 85 | )->getModuleByPath($filePath); 86 | 87 | $allowedModules = (array)DataObjectAnnotator::config()->get('enabled_modules'); 88 | 89 | return in_array($module->getName(), $allowedModules, true); 90 | } 91 | 92 | return false; 93 | } 94 | 95 | /** 96 | * Check if a (subclass of ) class is a supported 97 | * 98 | * @param $className 99 | * @return bool 100 | */ 101 | public function classNameIsSupported($className) 102 | { 103 | foreach ($this->supportedParentClasses as $supportedParent) { 104 | if (is_subclass_of($className, $supportedParent)) { 105 | return true; 106 | } 107 | } 108 | 109 | return false; 110 | } 111 | 112 | /** 113 | * Check if a module is in the $allowed_modules array 114 | * Required for the buildTask. 115 | * 116 | * @param string $moduleName 117 | * 118 | * @return bool 119 | */ 120 | public function moduleIsAllowed($moduleName) 121 | { 122 | return in_array($moduleName, $this->enabledModules(), false); 123 | } 124 | 125 | /** 126 | * @return array 127 | */ 128 | public function enabledModules() 129 | { 130 | $enabled = (array)DataObjectAnnotator::config()->get('enabled_modules'); 131 | 132 | // modules might be enabled more then once. 133 | return array_combine($enabled, $enabled); 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /src/Generators/DocBlockGenerator.php: -------------------------------------------------------------------------------- 1 | className = $className; 54 | $this->reflector = new ReflectionClass($className); 55 | $this->docBlockFactory = DocBlockFactory::createInstance(); 56 | 57 | $generatorClass = $this->reflector->isSubclassOf(Controller::class) 58 | ? ControllerTagGenerator::class : OrmTagGenerator::class; 59 | 60 | $this->tagGenerator = new $generatorClass($className, $this->getExistingTags()); 61 | } 62 | 63 | /** 64 | * @return Tag[] 65 | * @throws InvalidArgumentException 66 | */ 67 | public function getExistingTags() 68 | { 69 | $docBlock = $this->getExistingDocBlock(); 70 | if (!$docBlock) { 71 | return []; 72 | } 73 | 74 | $docBlock = $this->docBlockFactory->create($docBlock); 75 | 76 | return $docBlock->getTags(); 77 | } 78 | 79 | /** 80 | * Not that in case there are multiple doblocks for a class, 81 | * the last one will be returned 82 | * 83 | * If we file old style generated docblocks we remove them 84 | * 85 | * @return bool|string 86 | */ 87 | public function getExistingDocBlock() 88 | { 89 | return $this->reflector->getDocComment(); 90 | } 91 | 92 | /** 93 | * @return DocBlock|string 94 | * @throws LogicException 95 | * @throws InvalidArgumentException 96 | */ 97 | public function getGeneratedDocBlock() 98 | { 99 | $docBlock = $this->getExistingDocBlock(); 100 | 101 | return $this->mergeGeneratedTagsIntoDocBlock($docBlock); 102 | } 103 | 104 | /** 105 | * @param string $existingDocBlock 106 | * @return string 107 | * @throws LogicException 108 | * @throws InvalidArgumentException 109 | */ 110 | protected function mergeGeneratedTagsIntoDocBlock($existingDocBlock) 111 | { 112 | $docBlock = $this->docBlockFactory->create(($existingDocBlock ?: "/**\n*/")); 113 | 114 | $summary = $docBlock->getSummary(); 115 | if (!$summary) { 116 | $summary = sprintf('Class \\%s', $this->className); 117 | } 118 | 119 | $generatedTags = $this->getGeneratedTags(); 120 | $mergedTags = []; 121 | foreach ($generatedTags as $generatedTag) { 122 | $currentTag = $docBlock->getTagsByName($generatedTag->getName())[0] ?? null; 123 | 124 | // If there is an existing tag with the same name, preserve its description 125 | // There is no setDescription method so we use reflection 126 | if ($currentTag && $currentTag instanceof BaseTag && $currentTag->getDescription()) { 127 | $refObject = new ReflectionObject($generatedTag); 128 | if ($refObject->hasProperty('description')) { 129 | // If the property exists, we can set it 130 | $refProperty = $refObject->getProperty('description'); 131 | $refProperty->setAccessible(true); 132 | $refProperty->setValue($generatedTag, $currentTag->getDescription()); 133 | } 134 | } 135 | $mergedTags[] = $generatedTag; 136 | } 137 | foreach ($docBlock->getTags() as $existingTag) { 138 | // Skip any property, method or mixin tag 139 | if ($existingTag instanceof Property || $existingTag instanceof Method || $existingTag instanceof Mixin) { 140 | continue; 141 | } 142 | $mergedTags[] = $existingTag; 143 | } 144 | 145 | $docBlock = new DocBlock($summary, $docBlock->getDescription(), $mergedTags); 146 | 147 | $serializer = new Serializer(); 148 | $docBlock = $serializer->getDocComment($docBlock); 149 | 150 | return $docBlock; 151 | } 152 | 153 | /** 154 | * Remove all existing tags that are supported by this module. 155 | * 156 | * This will make sure that removed ORM properties and Extenions will not remain in the docblock, 157 | * while providing the option to manually add docblocks like @author etc. 158 | * 159 | * @param $docBlock 160 | * @return string 161 | */ 162 | public function removeExistingSupportedTags($docBlock) 163 | { 164 | $replacements = [ 165 | "/ \* @property ([\s\S]*?)\n/", 166 | "/ \* @method ([\s\S]*?)\n/", 167 | "/ \* @mixin ([\s\S]*?)\n/" 168 | ]; 169 | 170 | return (string)preg_replace($replacements, '', $docBlock); 171 | } 172 | 173 | /** 174 | * @return Tag[] 175 | */ 176 | public function getGeneratedTags() 177 | { 178 | return $this->tagGenerator->getTags(); 179 | } 180 | } 181 | -------------------------------------------------------------------------------- /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/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_filter((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/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 | --------------------------------------------------------------------------------