├── 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 | 
4 | [](https://scrutinizer-ci.com/g/silverleague/silverstripe-ideannotator/)
5 | [](https://codecov.io/gh/silverleague/silverstripe-ideannotator)
6 | [](https://packagist.org/packages/silverleague/ideannotator)
7 | [](https://packagist.org/packages/silverleague/ideannotator)
8 | [](https://packagist.org/packages/silverleague/ideannotator)
9 |
10 |
11 | This module generates @property, @method and @mixin tags for DataObjects, PageControllers and (Data)Extensions, so ide's like PHPStorm recognize the database and relations that are set in the $db, $has_one, $has_many and $many_many arrays.
12 |
13 | The docblocks can be generated/updated with each dev/build and with a DataObjectAnnotatorTask per module or classname.
14 |
15 | ## Requirements
16 |
17 | SilverStripe Framework and possible custom code.
18 |
19 | By default, `mysite` and `app` are enabled "modules".
20 |
21 | ### Version ^2:
22 | SilverStripe 3.x framework
23 |
24 | ### Version ^3:
25 | Silverstripe 4.x, 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 |
--------------------------------------------------------------------------------