├── .laminas-ci.json ├── COPYRIGHT.md ├── LICENSE.md ├── README.md ├── composer.json └── src ├── DeclareStatement.php ├── Exception ├── BadMethodCallException.php ├── ExceptionInterface.php ├── InvalidArgumentException.php └── RuntimeException.php ├── Generator ├── AbstractGenerator.php ├── AbstractMemberGenerator.php ├── BodyGenerator.php ├── ClassGenerator.php ├── DocBlock │ ├── Tag.php │ ├── Tag │ │ ├── AbstractTypeableTag.php │ │ ├── AuthorTag.php │ │ ├── GenericTag.php │ │ ├── LicenseTag.php │ │ ├── MethodTag.php │ │ ├── ParamTag.php │ │ ├── PropertyTag.php │ │ ├── ReturnTag.php │ │ ├── TagInterface.php │ │ ├── ThrowsTag.php │ │ └── VarTag.php │ └── TagManager.php ├── DocBlockGenerator.php ├── EnumGenerator │ ├── Cases │ │ ├── BackedCases.php │ │ ├── CaseFactory.php │ │ └── PureCases.php │ ├── EnumGenerator.php │ └── Name.php ├── Exception │ ├── ClassNotFoundException.php │ ├── ExceptionInterface.php │ ├── InvalidArgumentException.php │ └── RuntimeException.php ├── FileGenerator.php ├── GeneratorInterface.php ├── InterfaceGenerator.php ├── MethodGenerator.php ├── ParameterGenerator.php ├── PromotedParameterGenerator.php ├── PropertyGenerator.php ├── PropertyValueGenerator.php ├── TraitGenerator.php ├── TraitUsageGenerator.php ├── TraitUsageInterface.php ├── TypeGenerator.php ├── TypeGenerator │ ├── AtomicType.php │ ├── CompositeType.php │ ├── IntersectionType.php │ └── UnionType.php └── ValueGenerator.php ├── Generic └── Prototype │ ├── PrototypeClassFactory.php │ ├── PrototypeGenericInterface.php │ └── PrototypeInterface.php ├── Reflection ├── ClassReflection.php ├── DocBlock │ ├── Tag │ │ ├── AuthorTag.php │ │ ├── GenericTag.php │ │ ├── LicenseTag.php │ │ ├── MethodTag.php │ │ ├── ParamTag.php │ │ ├── PhpDocTypedTagInterface.php │ │ ├── PropertyTag.php │ │ ├── ReturnTag.php │ │ ├── TagInterface.php │ │ ├── ThrowsTag.php │ │ └── VarTag.php │ └── TagManager.php ├── DocBlockReflection.php ├── Exception │ ├── BadMethodCallException.php │ ├── ExceptionInterface.php │ ├── InvalidArgumentException.php │ └── RuntimeException.php ├── FunctionReflection.php ├── MethodReflection.php ├── ParameterReflection.php ├── PropertyReflection.php └── ReflectionInterface.php └── Scanner └── DocBlockScanner.php /.laminas-ci.json: -------------------------------------------------------------------------------- 1 | { 2 | "ignore_php_platform_requirements": { 3 | "8.4": true 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /COPYRIGHT.md: -------------------------------------------------------------------------------- 1 | Copyright (c) 2020 Laminas Project a Series of LF Projects, LLC. (https://getlaminas.org/) 2 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright (c) 2020 Laminas Project a Series of LF Projects, LLC. 2 | 3 | Redistribution and use in source and binary forms, with or without 4 | modification, are permitted provided that the following conditions are met: 5 | 6 | - Redistributions of source code must retain the above copyright notice, this 7 | list of conditions and the following disclaimer. 8 | 9 | - Redistributions in binary form must reproduce the above copyright notice, 10 | this list of conditions and the following disclaimer in the documentation 11 | and/or other materials provided with the distribution. 12 | 13 | - Neither the name of Laminas Foundation nor the names of its contributors may 14 | be used to endorse or promote products derived from this software without 15 | specific prior written permission. 16 | 17 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 18 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 19 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 20 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR 21 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 22 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 23 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON 24 | ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 25 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 26 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 27 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # laminas-code 2 | 3 | [![Continuous Integration](https://github.com/laminas/laminas-code/actions/workflows/continuous-integration.yml/badge.svg)](https://github.com/laminas/laminas-code/actions/workflows/continuous-integration.yml) 4 | [![Type Coverage](https://shepherd.dev/github/laminas/laminas-code/coverage.svg)](https://shepherd.dev/github/laminas/laminas-code) 5 | 6 | > ## 🇷🇺 Русским гражданам 7 | > 8 | > Мы, участники Laminas, родились и живем в разных странах. У многих из нас есть друзья, родственники и коллеги как в России, так и в Украине. Некоторые из нас родились в России. Некоторые из нас живут в России. У некоторых бабушки и дедушки сражались с фашистами во Второй мировой войне. Здесь никто не поддерживает фашизм. 9 | > 10 | > У одного из нас есть украинская родственница, которая спаслась из дома вместе с сыном. Поезд задержался из-за бомбежки на дороге впереди. У нас есть друзья, которые прячутся в бомбоубежищах. Мы с тревогой ждем весточки от них после воздушных налетов, которые беспорядочно наносят удары и попадают по больницам, школам, детским садам и домам. Мы не берем это из каких-либо СМИ. Мы наблюдаем это напрямую. 11 | > 12 | > Вы доверяете нам достаточно, чтоб использовать наши программы, и мы просим вас довериться нам вновь. Мы нуждаемся в помощи. Выходите и протестуйте против этой бесполезной войны. Остановите кровопролитие. Скажите "Нет войне!" 13 | > 14 | > ## 🇺🇸 To Citizens of Russia 15 | > 16 | > We at Laminas come from all over the world. Many of us have friends, family and colleagues in both Russia and Ukraine. Some of us were born in Russia. Some of us currently live in Russia. Some have grandparents who fought Nazis in World War II. Nobody here supports fascism. 17 | > 18 | > One team member has a Ukrainian relative who fled her home with her son. The train was delayed due to bombing on the road ahead. We have friends who are hiding in bomb shelters. We anxiously follow up on them after the air raids, which indiscriminately fire at hospitals, schools, kindergartens and houses. We're not taking this from any media. These are our actual experiences. 19 | > 20 | > You trust us enough to use our software. We ask that you trust us to say the truth on this. We need your help. Go out and protest this unnecessary war. Stop the bloodshed. Say "stop the war!" 21 | 22 | `Laminas\Code\Generator` provides facilities to generate arbitrary code using an 23 | object-oriented interface, both to create new code as well as to update existing 24 | code. While the current implementation is limited to generating PHP code, you 25 | can easily extend the base class in order to provide code generation for other 26 | tasks: JavaScript, configuration files, apache vhosts, etc. 27 | 28 | - File issues at https://github.com/laminas/laminas-code/issues 29 | - Documentation is at https://docs.laminas.dev/laminas-code/ 30 | - Migration documentation from v2 to v3 is at https://docs.laminas.dev/laminas-code/migration/ 31 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "laminas/laminas-code", 3 | "description": "Extensions to the PHP Reflection API, static code scanning, and code generation", 4 | "keywords": [ 5 | "laminas", 6 | "laminasframework", 7 | "code" 8 | ], 9 | "homepage": "https://laminas.dev", 10 | "license": "BSD-3-Clause", 11 | "require": { 12 | "php": "~8.1.0 || ~8.2.0 || ~8.3.0 || ~8.4.0" 13 | }, 14 | "require-dev": { 15 | "ext-phar": "*", 16 | "doctrine/annotations": "^2.0.1", 17 | "laminas/laminas-coding-standard": "^3.0.0", 18 | "laminas/laminas-stdlib": "^3.18.0", 19 | "phpunit/phpunit": "^10.5.37", 20 | "psalm/plugin-phpunit": "^0.19.0", 21 | "vimeo/psalm": "^5.15.0" 22 | }, 23 | "suggest": { 24 | "doctrine/annotations": "Doctrine\\Common\\Annotations >=1.0 for annotation features", 25 | "laminas/laminas-stdlib": "Laminas\\Stdlib component" 26 | }, 27 | "config": { 28 | "allow-plugins": { 29 | "dealerdirect/phpcodesniffer-composer-installer": true 30 | }, 31 | "platform": { 32 | "php": "8.1.99" 33 | }, 34 | "sort-packages": true 35 | }, 36 | "autoload": { 37 | "psr-4": { 38 | "Laminas\\Code\\": "src/" 39 | } 40 | }, 41 | "autoload-dev": { 42 | "psr-4": { 43 | "LaminasTest\\Code\\": "test/" 44 | } 45 | }, 46 | "scripts": { 47 | "check": [ 48 | "@cs-check", 49 | "@test" 50 | ], 51 | "cs-check": "phpcs", 52 | "cs-fix": "phpcbf", 53 | "static-analysis": "psalm --shepherd --stats", 54 | "test": "phpunit --colors=always", 55 | "test-coverage": "phpunit --colors=always --coverage-clover clover.xml" 56 | }, 57 | "support": { 58 | "issues": "https://github.com/laminas/laminas-code/issues", 59 | "forum": "https://discourse.laminas.dev", 60 | "chat": "https://laminas.dev/chat", 61 | "source": "https://github.com/laminas/laminas-code", 62 | "docs": "https://docs.laminas.dev/laminas-code/", 63 | "rss": "https://github.com/laminas/laminas-code/releases.atom" 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/DeclareStatement.php: -------------------------------------------------------------------------------- 1 | 'integer', 25 | self::STRICT_TYPES => 'integer', 26 | self::ENCODING => 'string', 27 | ]; 28 | 29 | /** @param int|string $value */ 30 | private function __construct(protected string $directive, protected $value) 31 | { 32 | } 33 | 34 | public function getDirective(): string 35 | { 36 | return $this->directive; 37 | } 38 | 39 | /** 40 | * @return int|string 41 | */ 42 | public function getValue() 43 | { 44 | return $this->value; 45 | } 46 | 47 | public static function ticks(int $value): self 48 | { 49 | return new self(self::TICKS, $value); 50 | } 51 | 52 | public static function strictTypes(int $value): self 53 | { 54 | return new self(self::STRICT_TYPES, $value); 55 | } 56 | 57 | public static function encoding(string $value): self 58 | { 59 | return new self(self::ENCODING, $value); 60 | } 61 | 62 | /** 63 | * @deprecated this API is deprecated, and will be removed in the next major release. Please 64 | * use the other constructors of this class instead. 65 | */ 66 | public static function fromArray(array $config): self 67 | { 68 | $directive = key($config); 69 | $value = $config[$directive]; 70 | 71 | if (! isset(self::ALLOWED[$directive])) { 72 | throw new InvalidArgumentException( 73 | sprintf( 74 | 'Declare directive must be one of: %s.', 75 | implode(', ', array_keys(self::ALLOWED)) 76 | ) 77 | ); 78 | } 79 | 80 | if (gettype($value) !== self::ALLOWED[$directive]) { 81 | throw new InvalidArgumentException( 82 | sprintf( 83 | 'Declare value invalid. Expected %s, got %s.', 84 | self::ALLOWED[$directive], 85 | gettype($value) 86 | ) 87 | ); 88 | } 89 | 90 | $method = str_replace('_', '', lcfirst(ucwords($directive, '_'))); 91 | 92 | return self::{$method}($value); 93 | } 94 | 95 | public function getStatement(): string 96 | { 97 | $value = is_string($this->value) ? '\'' . $this->value . '\'' : $this->value; 98 | 99 | return sprintf('declare(%s=%s);', $this->directive, $value); 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /src/Exception/BadMethodCallException.php: -------------------------------------------------------------------------------- 1 | setOptions($options); 36 | } 37 | } 38 | 39 | /** 40 | * @param bool $isSourceDirty 41 | * @return static 42 | */ 43 | public function setSourceDirty($isSourceDirty = true) 44 | { 45 | $this->isSourceDirty = (bool) $isSourceDirty; 46 | return $this; 47 | } 48 | 49 | /** 50 | * @return bool 51 | */ 52 | public function isSourceDirty() 53 | { 54 | return $this->isSourceDirty; 55 | } 56 | 57 | /** 58 | * @param string $indentation 59 | * @return static 60 | */ 61 | public function setIndentation($indentation) 62 | { 63 | $this->indentation = (string) $indentation; 64 | return $this; 65 | } 66 | 67 | /** 68 | * @return string 69 | */ 70 | public function getIndentation() 71 | { 72 | return $this->indentation; 73 | } 74 | 75 | /** 76 | * @param ?string $sourceContent 77 | * @return static 78 | */ 79 | public function setSourceContent($sourceContent) 80 | { 81 | $this->sourceContent = (string) $sourceContent; 82 | return $this; 83 | } 84 | 85 | /** 86 | * @return ?string 87 | */ 88 | public function getSourceContent() 89 | { 90 | return $this->sourceContent; 91 | } 92 | 93 | /** 94 | * @param array|Traversable $options 95 | * @throws Exception\InvalidArgumentException 96 | * @return static 97 | */ 98 | public function setOptions($options) 99 | { 100 | if (! is_array($options) && ! $options instanceof Traversable) { 101 | throw new Exception\InvalidArgumentException(sprintf( 102 | '%s expects an array or Traversable object; received "%s"', 103 | __METHOD__, 104 | get_debug_type($options) 105 | )); 106 | } 107 | 108 | foreach ($options as $optionName => $optionValue) { 109 | $methodName = 'set' . $optionName; 110 | if (method_exists($this, $methodName)) { 111 | $this->{$methodName}($optionValue); 112 | } 113 | } 114 | 115 | return $this; 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /src/Generator/AbstractMemberGenerator.php: -------------------------------------------------------------------------------- 1 | flags = $flags; 43 | 44 | return $this; 45 | } 46 | 47 | /** 48 | * @param int $flag 49 | * @return static 50 | */ 51 | public function addFlag($flag) 52 | { 53 | $this->setFlags($this->flags | $flag); 54 | return $this; 55 | } 56 | 57 | /** 58 | * @param int $flag 59 | * @return static 60 | */ 61 | public function removeFlag($flag) 62 | { 63 | $this->setFlags($this->flags & ~$flag); 64 | return $this; 65 | } 66 | 67 | /** 68 | * @param bool $isAbstract 69 | * @return static 70 | */ 71 | public function setAbstract($isAbstract) 72 | { 73 | return $isAbstract ? $this->addFlag(self::FLAG_ABSTRACT) : $this->removeFlag(self::FLAG_ABSTRACT); 74 | } 75 | 76 | /** 77 | * @return bool 78 | */ 79 | public function isAbstract() 80 | { 81 | return (bool) ($this->flags & self::FLAG_ABSTRACT); 82 | } 83 | 84 | /** 85 | * @param bool $isInterface 86 | * @return static 87 | */ 88 | public function setInterface($isInterface) 89 | { 90 | return $isInterface ? $this->addFlag(self::FLAG_INTERFACE) : $this->removeFlag(self::FLAG_INTERFACE); 91 | } 92 | 93 | /** 94 | * @return bool 95 | */ 96 | public function isInterface() 97 | { 98 | return (bool) ($this->flags & self::FLAG_INTERFACE); 99 | } 100 | 101 | /** 102 | * @param bool $isFinal 103 | * @return static 104 | */ 105 | public function setFinal($isFinal) 106 | { 107 | return $isFinal ? $this->addFlag(self::FLAG_FINAL) : $this->removeFlag(self::FLAG_FINAL); 108 | } 109 | 110 | /** 111 | * @return bool 112 | */ 113 | public function isFinal() 114 | { 115 | return (bool) ($this->flags & self::FLAG_FINAL); 116 | } 117 | 118 | /** 119 | * @param bool $isStatic 120 | * @return static 121 | */ 122 | public function setStatic($isStatic) 123 | { 124 | return $isStatic ? $this->addFlag(self::FLAG_STATIC) : $this->removeFlag(self::FLAG_STATIC); 125 | } 126 | 127 | /** 128 | * @return bool 129 | */ 130 | public function isStatic() 131 | { 132 | return (bool) ($this->flags & self::FLAG_STATIC); // is FLAG_STATIC in flags 133 | } 134 | 135 | /** 136 | * @param string $visibility 137 | * @return static 138 | */ 139 | public function setVisibility($visibility) 140 | { 141 | switch ($visibility) { 142 | case self::VISIBILITY_PUBLIC: 143 | $this->removeFlag(self::FLAG_PRIVATE | self::FLAG_PROTECTED); // remove both 144 | $this->addFlag(self::FLAG_PUBLIC); 145 | break; 146 | case self::VISIBILITY_PROTECTED: 147 | $this->removeFlag(self::FLAG_PUBLIC | self::FLAG_PRIVATE); // remove both 148 | $this->addFlag(self::FLAG_PROTECTED); 149 | break; 150 | case self::VISIBILITY_PRIVATE: 151 | $this->removeFlag(self::FLAG_PUBLIC | self::FLAG_PROTECTED); // remove both 152 | $this->addFlag(self::FLAG_PRIVATE); 153 | break; 154 | } 155 | 156 | return $this; 157 | } 158 | 159 | /** 160 | * @psalm-return static::VISIBILITY_* 161 | */ 162 | public function getVisibility() 163 | { 164 | switch (true) { 165 | case $this->flags & self::FLAG_PROTECTED: 166 | return self::VISIBILITY_PROTECTED; 167 | case $this->flags & self::FLAG_PRIVATE: 168 | return self::VISIBILITY_PRIVATE; 169 | default: 170 | return self::VISIBILITY_PUBLIC; 171 | } 172 | } 173 | 174 | /** 175 | * @param string $name 176 | * @return static 177 | */ 178 | public function setName($name) 179 | { 180 | $this->name = (string) $name; 181 | return $this; 182 | } 183 | 184 | /** 185 | * @return string 186 | */ 187 | public function getName() 188 | { 189 | return $this->name; 190 | } 191 | 192 | /** 193 | * @param DocBlockGenerator|string $docBlock 194 | * @throws Exception\InvalidArgumentException 195 | * @return static 196 | */ 197 | public function setDocBlock($docBlock) 198 | { 199 | if (is_string($docBlock)) { 200 | $docBlock = new DocBlockGenerator($docBlock); 201 | } elseif (! $docBlock instanceof DocBlockGenerator) { 202 | throw new Exception\InvalidArgumentException(sprintf( 203 | '%s is expecting either a string, array or an instance of %s\DocBlockGenerator', 204 | __METHOD__, 205 | __NAMESPACE__ 206 | )); 207 | } 208 | 209 | $this->docBlock = $docBlock; 210 | 211 | return $this; 212 | } 213 | 214 | public function removeDocBlock(): void 215 | { 216 | $this->docBlock = null; 217 | } 218 | 219 | /** 220 | * @return DocBlockGenerator|null 221 | */ 222 | public function getDocBlock() 223 | { 224 | return $this->docBlock; 225 | } 226 | } 227 | -------------------------------------------------------------------------------- /src/Generator/BodyGenerator.php: -------------------------------------------------------------------------------- 1 | content = (string) $content; 16 | return $this; 17 | } 18 | 19 | /** 20 | * @return string 21 | */ 22 | public function getContent() 23 | { 24 | return $this->content; 25 | } 26 | 27 | /** 28 | * @return string 29 | */ 30 | public function generate() 31 | { 32 | return $this->getContent(); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/Generator/DocBlock/Tag.php: -------------------------------------------------------------------------------- 1 | initializeDefaultTags(); 22 | return $tagManager->createTagFromReflection($reflectionTag); 23 | } 24 | 25 | /** 26 | * @deprecated Deprecated in 2.3. Use GenericTag::setContent() instead 27 | * 28 | * @param string $description 29 | * @return Tag 30 | */ 31 | public function setDescription($description) 32 | { 33 | return $this->setContent($description); 34 | } 35 | 36 | /** 37 | * @deprecated Deprecated in 2.3. Use GenericTag::getContent() instead 38 | * 39 | * @return string|null 40 | */ 41 | public function getDescription() 42 | { 43 | return $this->getContent(); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/Generator/DocBlock/Tag/AbstractTypeableTag.php: -------------------------------------------------------------------------------- 1 | setTypes($types); 33 | } 34 | 35 | if (! empty($description)) { 36 | $this->setDescription($description); 37 | } 38 | } 39 | 40 | /** 41 | * @param string $description 42 | * @return AbstractTypeableTag 43 | */ 44 | public function setDescription($description) 45 | { 46 | $this->description = $description; 47 | return $this; 48 | } 49 | 50 | /** 51 | * @return string|null 52 | */ 53 | public function getDescription() 54 | { 55 | return $this->description; 56 | } 57 | 58 | /** 59 | * Array of types or string with types delimited by pipe (|) 60 | * e.g. array('int', 'null') or "int|null" 61 | * 62 | * @param string[]|string $types 63 | * @return AbstractTypeableTag 64 | */ 65 | public function setTypes($types) 66 | { 67 | if (is_string($types)) { 68 | $types = explode('|', $types); 69 | } 70 | $this->types = $types; 71 | return $this; 72 | } 73 | 74 | /** 75 | * @return string[] 76 | */ 77 | public function getTypes() 78 | { 79 | return $this->types; 80 | } 81 | 82 | /** 83 | * @param string $delimiter 84 | * @return string 85 | */ 86 | public function getTypesAsString($delimiter = '|') 87 | { 88 | return implode($delimiter, $this->types); 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /src/Generator/DocBlock/Tag/AuthorTag.php: -------------------------------------------------------------------------------- 1 | setAuthorName($authorName); 25 | } 26 | 27 | if (! empty($authorEmail)) { 28 | $this->setAuthorEmail($authorEmail); 29 | } 30 | } 31 | 32 | /** 33 | * @deprecated Deprecated in 2.3. Use TagManager::createTagFromReflection() instead 34 | * 35 | * @return AuthorTag 36 | */ 37 | public static function fromReflection(ReflectionTagInterface $reflectionTag) 38 | { 39 | $tagManager = new TagManager(); 40 | $tagManager->initializeDefaultTags(); 41 | return $tagManager->createTagFromReflection($reflectionTag); 42 | } 43 | 44 | /** @return 'author' */ 45 | public function getName() 46 | { 47 | return 'author'; 48 | } 49 | 50 | /** 51 | * @param string $authorEmail 52 | * @return AuthorTag 53 | */ 54 | public function setAuthorEmail($authorEmail) 55 | { 56 | $this->authorEmail = $authorEmail; 57 | return $this; 58 | } 59 | 60 | /** @return string|null */ 61 | public function getAuthorEmail() 62 | { 63 | return $this->authorEmail; 64 | } 65 | 66 | /** 67 | * @param string $authorName 68 | * @return AuthorTag 69 | */ 70 | public function setAuthorName($authorName) 71 | { 72 | $this->authorName = $authorName; 73 | return $this; 74 | } 75 | 76 | /** @return string|null */ 77 | public function getAuthorName() 78 | { 79 | return $this->authorName; 80 | } 81 | 82 | /** @return non-empty-string */ 83 | public function generate() 84 | { 85 | return '@author' 86 | . (! empty($this->authorName) ? ' ' . $this->authorName : '') 87 | . (! empty($this->authorEmail) ? ' <' . $this->authorEmail . '>' : ''); 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /src/Generator/DocBlock/Tag/GenericTag.php: -------------------------------------------------------------------------------- 1 | setName($name); 26 | } 27 | 28 | if (! empty($content)) { 29 | $this->setContent($content); 30 | } 31 | } 32 | 33 | /** 34 | * @param string $name 35 | * @return $this 36 | */ 37 | public function setName($name) 38 | { 39 | $this->name = ltrim($name, '@'); 40 | return $this; 41 | } 42 | 43 | /** @return string|null */ 44 | public function getName() 45 | { 46 | return $this->name; 47 | } 48 | 49 | /** 50 | * @param string $content 51 | * @return $this 52 | */ 53 | public function setContent($content) 54 | { 55 | $this->content = $content; 56 | return $this; 57 | } 58 | 59 | /** @return string|null */ 60 | public function getContent() 61 | { 62 | return $this->content; 63 | } 64 | 65 | /** @return non-empty-string */ 66 | public function generate() 67 | { 68 | return '@' . $this->name 69 | . (! empty($this->content) ? ' ' . $this->content : ''); 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/Generator/DocBlock/Tag/LicenseTag.php: -------------------------------------------------------------------------------- 1 | setUrl($url); 25 | } 26 | 27 | if (! empty($licenseName)) { 28 | $this->setLicenseName($licenseName); 29 | } 30 | } 31 | 32 | /** 33 | * @deprecated Deprecated in 2.3. Use TagManager::createTagFromReflection() instead 34 | * 35 | * @return ReturnTag 36 | */ 37 | public static function fromReflection(ReflectionTagInterface $reflectionTag) 38 | { 39 | $tagManager = new TagManager(); 40 | $tagManager->initializeDefaultTags(); 41 | return $tagManager->createTagFromReflection($reflectionTag); 42 | } 43 | 44 | /** @return 'license' */ 45 | public function getName() 46 | { 47 | return 'license'; 48 | } 49 | 50 | /** 51 | * @param string $url 52 | * @return LicenseTag 53 | */ 54 | public function setUrl($url) 55 | { 56 | $this->url = $url; 57 | return $this; 58 | } 59 | 60 | /** @return string|null */ 61 | public function getUrl() 62 | { 63 | return $this->url; 64 | } 65 | 66 | /** 67 | * @param string $name 68 | * @return LicenseTag 69 | */ 70 | public function setLicenseName($name) 71 | { 72 | $this->licenseName = $name; 73 | return $this; 74 | } 75 | 76 | /** @return string|null */ 77 | public function getLicenseName() 78 | { 79 | return $this->licenseName; 80 | } 81 | 82 | /** @return non-empty-string */ 83 | public function generate() 84 | { 85 | return '@license' 86 | . (! empty($this->url) ? ' ' . $this->url : '') 87 | . (! empty($this->licenseName) ? ' ' . $this->licenseName : ''); 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /src/Generator/DocBlock/Tag/MethodTag.php: -------------------------------------------------------------------------------- 1 | setMethodName($methodName); 25 | } 26 | 27 | $this->setIsStatic((bool) $isStatic); 28 | 29 | parent::__construct($types, $description); 30 | } 31 | 32 | /** 33 | * @return string 34 | */ 35 | public function getName() 36 | { 37 | return 'method'; 38 | } 39 | 40 | /** 41 | * @param bool $isStatic 42 | * @return MethodTag 43 | */ 44 | public function setIsStatic($isStatic) 45 | { 46 | $this->isStatic = $isStatic; 47 | return $this; 48 | } 49 | 50 | /** 51 | * @return bool 52 | */ 53 | public function isStatic() 54 | { 55 | return $this->isStatic; 56 | } 57 | 58 | /** 59 | * @param non-empty-string $methodName 60 | * @return MethodTag 61 | */ 62 | public function setMethodName($methodName) 63 | { 64 | $this->methodName = rtrim($methodName, ')('); 65 | return $this; 66 | } 67 | 68 | /** @return string|null */ 69 | public function getMethodName() 70 | { 71 | return $this->methodName; 72 | } 73 | 74 | /** @return non-empty-string */ 75 | public function generate() 76 | { 77 | return '@method' 78 | . ($this->isStatic ? ' static' : '') 79 | . (! empty($this->types) ? ' ' . $this->getTypesAsString() : '') 80 | . (! empty($this->methodName) ? ' ' . $this->methodName . '()' : '') 81 | . (! empty($this->description) ? ' ' . $this->description : ''); 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /src/Generator/DocBlock/Tag/ParamTag.php: -------------------------------------------------------------------------------- 1 | setVariableName($variableName); 24 | } 25 | 26 | parent::__construct($types, $description); 27 | } 28 | 29 | /** 30 | * @deprecated Deprecated in 2.3. Use TagManager::createTagFromReflection() instead 31 | * 32 | * @return ParamTag 33 | */ 34 | public static function fromReflection(ReflectionTagInterface $reflectionTag) 35 | { 36 | $tagManager = new TagManager(); 37 | $tagManager->initializeDefaultTags(); 38 | return $tagManager->createTagFromReflection($reflectionTag); 39 | } 40 | 41 | /** 42 | * @return string 43 | */ 44 | public function getName() 45 | { 46 | return 'param'; 47 | } 48 | 49 | /** 50 | * @param string $variableName 51 | * @return ParamTag 52 | */ 53 | public function setVariableName($variableName) 54 | { 55 | $this->variableName = ltrim($variableName, '$'); 56 | return $this; 57 | } 58 | 59 | /** 60 | * @return string 61 | */ 62 | public function getVariableName() 63 | { 64 | return $this->variableName; 65 | } 66 | 67 | /** 68 | * @deprecated Deprecated in 2.3. Use setTypes() instead 69 | * 70 | * @param string $datatype 71 | * @return ParamTag 72 | */ 73 | public function setDatatype($datatype) 74 | { 75 | return $this->setTypes($datatype); 76 | } 77 | 78 | /** 79 | * @deprecated Deprecated in 2.3. Use getTypes() or getTypesAsString() instead 80 | * 81 | * @return string 82 | */ 83 | public function getDatatype() 84 | { 85 | return $this->getTypesAsString(); 86 | } 87 | 88 | /** 89 | * @deprecated Deprecated in 2.3. Use setVariableName() instead 90 | * 91 | * @param string $paramName 92 | * @return ParamTag 93 | */ 94 | public function setParamName($paramName) 95 | { 96 | return $this->setVariableName($paramName); 97 | } 98 | 99 | /** 100 | * @deprecated Deprecated in 2.3. Use getVariableName() instead 101 | * 102 | * @return string 103 | */ 104 | public function getParamName() 105 | { 106 | return $this->getVariableName(); 107 | } 108 | 109 | /** 110 | * @return string 111 | */ 112 | public function generate() 113 | { 114 | return '@param' 115 | . (! empty($this->types) ? ' ' . $this->getTypesAsString() : '') 116 | . (! empty($this->variableName) ? ' $' . $this->variableName : '') 117 | . (! empty($this->description) ? ' ' . $this->description : ''); 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /src/Generator/DocBlock/Tag/PropertyTag.php: -------------------------------------------------------------------------------- 1 | setPropertyName($propertyName); 21 | } 22 | 23 | parent::__construct($types, $description); 24 | } 25 | 26 | /** 27 | * @return string 28 | */ 29 | public function getName() 30 | { 31 | return 'property'; 32 | } 33 | 34 | /** 35 | * @param string $propertyName 36 | * @return self 37 | */ 38 | public function setPropertyName($propertyName) 39 | { 40 | $this->propertyName = ltrim($propertyName, '$'); 41 | return $this; 42 | } 43 | 44 | /** 45 | * @return string|null 46 | */ 47 | public function getPropertyName() 48 | { 49 | return $this->propertyName; 50 | } 51 | 52 | /** 53 | * @return string 54 | */ 55 | public function generate() 56 | { 57 | return '@property' 58 | . (! empty($this->types) ? ' ' . $this->getTypesAsString() : '') 59 | . (! empty($this->propertyName) ? ' $' . $this->propertyName : '') 60 | . (! empty($this->description) ? ' ' . $this->description : ''); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/Generator/DocBlock/Tag/ReturnTag.php: -------------------------------------------------------------------------------- 1 | initializeDefaultTags(); 19 | return $tagManager->createTagFromReflection($reflectionTag); 20 | } 21 | 22 | /** 23 | * @return string 24 | */ 25 | public function getName() 26 | { 27 | return 'return'; 28 | } 29 | 30 | /** 31 | * @deprecated Deprecated in 2.3. Use setTypes() instead 32 | * 33 | * @param string $datatype 34 | * @return ReturnTag 35 | */ 36 | public function setDatatype($datatype) 37 | { 38 | return $this->setTypes($datatype); 39 | } 40 | 41 | /** 42 | * @deprecated Deprecated in 2.3. Use getTypes() or getTypesAsString() instead 43 | * 44 | * @return string 45 | */ 46 | public function getDatatype() 47 | { 48 | return $this->getTypesAsString(); 49 | } 50 | 51 | /** 52 | * @return string 53 | */ 54 | public function generate() 55 | { 56 | return '@return ' 57 | . $this->getTypesAsString() 58 | . (! empty($this->description) ? ' ' . $this->description : ''); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/Generator/DocBlock/Tag/TagInterface.php: -------------------------------------------------------------------------------- 1 | types) ? ' ' . $this->getTypesAsString() : '') 22 | . (! empty($this->description) ? ' ' . $this->description : ''); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/Generator/DocBlock/Tag/VarTag.php: -------------------------------------------------------------------------------- 1 | variableName = ltrim($variableName, '$'); 18 | } 19 | 20 | parent::__construct($types, $description); 21 | } 22 | 23 | /** @inheritDoc */ 24 | public function getName(): string 25 | { 26 | return 'var'; 27 | } 28 | 29 | /** 30 | * @internal this code is only public for compatibility with the 31 | * 32 | * @see \Laminas\Code\Generator\DocBlock\TagManager, which 33 | * uses setters 34 | */ 35 | public function setVariableName(?string $variableName): void 36 | { 37 | if (null !== $variableName) { 38 | $this->variableName = ltrim($variableName, '$'); 39 | } 40 | } 41 | 42 | public function getVariableName(): ?string 43 | { 44 | return $this->variableName; 45 | } 46 | 47 | /** @inheritDoc */ 48 | public function generate(): string 49 | { 50 | return '@var' 51 | . (! empty($this->types) ? ' ' . $this->getTypesAsString() : '') 52 | . (null !== $this->variableName ? ' $' . $this->variableName : '') 53 | . (! empty($this->description) ? ' ' . $this->description : ''); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/Generator/DocBlock/TagManager.php: -------------------------------------------------------------------------------- 1 | addPrototype(new Tag\ParamTag()); 33 | $this->addPrototype(new Tag\ReturnTag()); 34 | $this->addPrototype(new Tag\MethodTag()); 35 | $this->addPrototype(new Tag\PropertyTag()); 36 | $this->addPrototype(new Tag\AuthorTag()); 37 | $this->addPrototype(new Tag\LicenseTag()); 38 | $this->addPrototype(new Tag\ThrowsTag()); 39 | $this->addPrototype(new Tag\VarTag()); 40 | $this->setGenericPrototype(new Tag\GenericTag()); 41 | } 42 | 43 | /** 44 | * @return TagInterface 45 | */ 46 | public function createTagFromReflection(ReflectionTagInterface $reflectionTag) 47 | { 48 | $tagName = $reflectionTag->getName(); 49 | 50 | /** @var TagInterface $newTag */ 51 | $newTag = $this->getClonedPrototype($tagName); 52 | 53 | // transport any properties via accessors and mutators from reflection to codegen object 54 | $reflectionClass = new ReflectionClass($reflectionTag); 55 | foreach ($reflectionClass->getMethods(ReflectionMethod::IS_PUBLIC) as $method) { 56 | if (str_starts_with($method->getName(), 'get')) { 57 | $propertyName = substr($method->getName(), 3); 58 | if (method_exists($newTag, 'set' . $propertyName)) { 59 | $newTag->{'set' . $propertyName}($reflectionTag->{'get' . $propertyName}()); 60 | } 61 | } elseif (str_starts_with($method->getName(), 'is')) { 62 | $propertyName = ucfirst($method->getName()); 63 | if (method_exists($newTag, 'set' . $propertyName)) { 64 | $newTag->{'set' . $propertyName}($reflectionTag->{$method->getName()}()); 65 | } 66 | } 67 | } 68 | return $newTag; 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/Generator/DocBlockGenerator.php: -------------------------------------------------------------------------------- 1 | setSourceContent($reflectionDocBlock->getContents()); 42 | $docBlock->setSourceDirty(false); 43 | 44 | $docBlock->setShortDescription($reflectionDocBlock->getShortDescription()); 45 | $docBlock->setLongDescription($reflectionDocBlock->getLongDescription()); 46 | 47 | foreach ($reflectionDocBlock->getTags() as $tag) { 48 | $docBlock->setTag(self::getTagManager()->createTagFromReflection($tag)); 49 | } 50 | 51 | return $docBlock; 52 | } 53 | 54 | /** 55 | * Generate from array 56 | * 57 | * @deprecated this API is deprecated, and will be removed in the next major release. Please 58 | * use the other constructors of this class instead. 59 | * 60 | * @configkey shortdescription string The short description for this doc block 61 | * @configkey longdescription string The long description for this doc block 62 | * @configkey tags array 63 | * @throws Exception\InvalidArgumentException 64 | * @return DocBlockGenerator 65 | */ 66 | public static function fromArray(array $array) 67 | { 68 | $docBlock = new static(); 69 | 70 | foreach ($array as $name => $value) { 71 | // normalize key 72 | switch (strtolower(str_replace(['.', '-', '_'], '', $name))) { 73 | case 'shortdescription': 74 | $docBlock->setShortDescription($value); 75 | break; 76 | case 'longdescription': 77 | $docBlock->setLongDescription($value); 78 | break; 79 | case 'tags': 80 | $docBlock->setTags($value); 81 | break; 82 | } 83 | } 84 | 85 | return $docBlock; 86 | } 87 | 88 | /** 89 | * @return TagManager 90 | */ 91 | protected static function getTagManager() 92 | { 93 | if (! isset(static::$tagManager)) { 94 | static::$tagManager = new TagManager(); 95 | static::$tagManager->initializeDefaultTags(); 96 | } 97 | return static::$tagManager; 98 | } 99 | 100 | /** 101 | * @param ?string $shortDescription 102 | * @param ?string $longDescription 103 | * @param array[]|TagInterface[] $tags 104 | */ 105 | public function __construct($shortDescription = null, $longDescription = null, array $tags = []) 106 | { 107 | if ($shortDescription) { 108 | $this->setShortDescription($shortDescription); 109 | } 110 | if ($longDescription) { 111 | $this->setLongDescription($longDescription); 112 | } 113 | if ($tags) { 114 | $this->setTags($tags); 115 | } 116 | } 117 | 118 | /** 119 | * @param string $shortDescription 120 | * @return DocBlockGenerator 121 | */ 122 | public function setShortDescription($shortDescription) 123 | { 124 | $this->shortDescription = $shortDescription; 125 | return $this; 126 | } 127 | 128 | /** 129 | * @return string 130 | */ 131 | public function getShortDescription() 132 | { 133 | return $this->shortDescription; 134 | } 135 | 136 | /** 137 | * @param string $longDescription 138 | * @return DocBlockGenerator 139 | */ 140 | public function setLongDescription($longDescription) 141 | { 142 | $this->longDescription = $longDescription; 143 | return $this; 144 | } 145 | 146 | /** 147 | * @return string 148 | */ 149 | public function getLongDescription() 150 | { 151 | return $this->longDescription; 152 | } 153 | 154 | /** 155 | * @param array[]|TagInterface[] $tags 156 | * @return DocBlockGenerator 157 | */ 158 | public function setTags(array $tags) 159 | { 160 | foreach ($tags as $tag) { 161 | $this->setTag($tag); 162 | } 163 | 164 | return $this; 165 | } 166 | 167 | /** 168 | * @param array|TagInterface $tag 169 | * @throws Exception\InvalidArgumentException 170 | * @return DocBlockGenerator 171 | */ 172 | public function setTag($tag) 173 | { 174 | if (is_array($tag)) { 175 | // use deprecated Tag class for backward compatibility to old array-keys 176 | $genericTag = new Tag(); 177 | $genericTag->setOptions($tag); 178 | $tag = $genericTag; 179 | } elseif (! $tag instanceof TagInterface) { 180 | throw new Exception\InvalidArgumentException(sprintf( 181 | '%s expects either an array of method options or an instance of %s\DocBlock\Tag\TagInterface', 182 | __METHOD__, 183 | __NAMESPACE__ 184 | )); 185 | } 186 | 187 | $this->tags[] = $tag; 188 | return $this; 189 | } 190 | 191 | /** 192 | * @return TagInterface[] 193 | */ 194 | public function getTags() 195 | { 196 | return $this->tags; 197 | } 198 | 199 | /** 200 | * @param bool $value 201 | * @return DocBlockGenerator 202 | */ 203 | public function setWordWrap($value) 204 | { 205 | $this->wordwrap = (bool) $value; 206 | return $this; 207 | } 208 | 209 | /** 210 | * @return bool 211 | */ 212 | public function getWordWrap() 213 | { 214 | return $this->wordwrap; 215 | } 216 | 217 | /** 218 | * @return string 219 | */ 220 | public function generate() 221 | { 222 | if (! $this->isSourceDirty()) { 223 | return $this->docCommentize(trim($this->getSourceContent() ?? '')); 224 | } 225 | 226 | $output = ''; 227 | if ($sd = $this->getShortDescription()) { 228 | $output .= $sd . self::LINE_FEED . self::LINE_FEED; 229 | } 230 | if ($ld = $this->getLongDescription()) { 231 | $output .= $ld . self::LINE_FEED . self::LINE_FEED; 232 | } 233 | 234 | /** @var GeneratorInterface $tag */ 235 | foreach ($this->getTags() as $tag) { 236 | $output .= $tag->generate() . self::LINE_FEED; 237 | } 238 | 239 | return $this->docCommentize(trim($output)); 240 | } 241 | 242 | /** 243 | * @param string $content 244 | * @return string 245 | */ 246 | protected function docCommentize($content) 247 | { 248 | $indent = $this->getIndentation(); 249 | $output = $indent . '/**' . self::LINE_FEED; 250 | $content = $this->getWordWrap() == true ? wordwrap($content, 80, self::LINE_FEED) : $content; 251 | $lines = explode(self::LINE_FEED, $content); 252 | foreach ($lines as $line) { 253 | $output .= $indent . ' *'; 254 | if ($line) { 255 | $output .= ' ' . $line; 256 | } 257 | $output .= self::LINE_FEED; 258 | } 259 | $output .= $indent . ' */' . self::LINE_FEED; 260 | 261 | return $output; 262 | } 263 | } 264 | -------------------------------------------------------------------------------- /src/Generator/EnumGenerator/Cases/BackedCases.php: -------------------------------------------------------------------------------- 1 | $cases 20 | */ 21 | private function __construct(public readonly string $type, public readonly array $cases) 22 | { 23 | } 24 | 25 | /** 26 | * @param array|array $backedCases 27 | * @param 'int'|'string' $type 28 | */ 29 | public static function fromCasesWithType(array $backedCases, string $type): self 30 | { 31 | if (! ($type === 'int' || $type === 'string')) { 32 | throw new InvalidArgumentException(sprintf( 33 | '"%s" is not a valid type for Enums, only "int" and "string" types are allowed.', 34 | $type 35 | )); 36 | } 37 | 38 | $cases = []; 39 | foreach ($backedCases as $case => $value) { 40 | if ($type === 'string') { 41 | $value = sprintf("'%s'", $value); 42 | } 43 | 44 | $cases[] = $case . ' = ' . $value; 45 | } 46 | 47 | return new self($type, $cases); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/Generator/EnumGenerator/Cases/CaseFactory.php: -------------------------------------------------------------------------------- 1 | , 22 | * }|array{ 23 | * name: non-empty-string, 24 | * backedCases: array{ 25 | * type: 'int', 26 | * cases: array, 27 | * }|array{ 28 | * type: 'string', 29 | * cases: array, 30 | * }, 31 | * } $options 32 | * @return BackedCases|PureCases 33 | */ 34 | public static function fromOptions(array $options) 35 | { 36 | if (array_key_exists('pureCases', $options) && ! array_key_exists('backedCases', $options)) { 37 | return PureCases::fromCases($options['pureCases']); 38 | } 39 | 40 | assert(! array_key_exists('pureCases', $options) && array_key_exists('backedCases', $options)); 41 | return BackedCases::fromCasesWithType($options['backedCases']['cases'], $options['backedCases']['type']); 42 | } 43 | 44 | /** 45 | * @return BackedCases|PureCases 46 | */ 47 | public static function fromReflectionCases(ReflectionEnum $enum) 48 | { 49 | $backingType = $enum->getBackingType(); 50 | 51 | if ($backingType === null) { 52 | return PureCases::fromCases(array_map( 53 | /** @return non-empty-string */ 54 | static fn(ReflectionEnumUnitCase $singleCase): string => $singleCase->getName(), 55 | $enum->getCases() 56 | )); 57 | } 58 | 59 | assert($backingType instanceof ReflectionNamedType); 60 | 61 | $cases = $enum->getCases(); 62 | 63 | return BackedCases::fromCasesWithType( 64 | array_combine( 65 | array_map( 66 | /** @return non-empty-string */ 67 | static fn(ReflectionEnumBackedCase $case): string => $case->getName(), 68 | $cases 69 | ), 70 | array_map(static fn(ReflectionEnumBackedCase $case): string|int => $case->getBackingValue(), $cases), 71 | ), 72 | $backingType->getName() 73 | ); 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /src/Generator/EnumGenerator/Cases/PureCases.php: -------------------------------------------------------------------------------- 1 | $cases */ 13 | private function __construct(public readonly array $cases) 14 | { 15 | } 16 | 17 | /** 18 | * @param list $pureCases 19 | */ 20 | public static function fromCases(array $pureCases): self 21 | { 22 | return new self($pureCases); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/Generator/EnumGenerator/EnumGenerator.php: -------------------------------------------------------------------------------- 1 | name->getNamespace()) { 38 | $output .= 'namespace ' . $this->name->getNamespace() . ';' . self::LINE_FEED . self::LINE_FEED; 39 | } 40 | 41 | return $output . 'enum ' . $this->name->getName() . $this->retrieveType() . ' {' 42 | . self::LINE_FEED 43 | . $this->retrieveCases() 44 | . '}' 45 | . self::LINE_FEED; 46 | } 47 | 48 | private function retrieveType(): string 49 | { 50 | if ($this->cases instanceof BackedCases) { 51 | return ': ' . $this->cases->type; 52 | } 53 | 54 | return ''; 55 | } 56 | 57 | private function retrieveCases(): string 58 | { 59 | return implode( 60 | '', 61 | array_map( 62 | fn (string $case): string => self::INDENTATION . 'case ' . $case . ';' . self::LINE_FEED, 63 | $this->cases->cases 64 | ) 65 | ); 66 | } 67 | 68 | /** 69 | * @psalm-param array{ 70 | * name: non-empty-string, 71 | * pureCases: list, 72 | * }|array{ 73 | * name: non-empty-string, 74 | * backedCases: array{ 75 | * type: 'int'|'string', 76 | * cases: array, 77 | * }, 78 | * } $options 79 | */ 80 | public static function withConfig(array $options): self 81 | { 82 | return new self( 83 | Name::fromFullyQualifiedClassName($options['name']), 84 | CaseFactory::fromOptions($options), 85 | ); 86 | } 87 | 88 | public static function fromReflection(ReflectionEnum $enum): self 89 | { 90 | return new self( 91 | Name::fromFullyQualifiedClassName($enum->getName()), 92 | CaseFactory::fromReflectionCases($enum), 93 | ); 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /src/Generator/EnumGenerator/Name.php: -------------------------------------------------------------------------------- 1 | name; 22 | } 23 | 24 | public function getNamespace(): ?string 25 | { 26 | return $this->namespace; 27 | } 28 | 29 | public static function fromFullyQualifiedClassName(string $name): self 30 | { 31 | $namespace = null; 32 | $nsPosition = strrpos($name, '\\'); 33 | if (false !== $nsPosition) { 34 | $namespace = substr($name, 0, $nsPosition); 35 | $name = substr($name, $nsPosition + 1); 36 | } 37 | 38 | return new self($name, $namespace); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/Generator/Exception/ClassNotFoundException.php: -------------------------------------------------------------------------------- 1 | isInterface()) { 24 | throw new Exception\InvalidArgumentException(sprintf( 25 | 'Class %s is not a interface', 26 | $classReflection->getName() 27 | )); 28 | } 29 | 30 | // class generator 31 | $cg = new static($classReflection->getName()); 32 | $methods = []; 33 | 34 | $cg->setSourceContent($cg->getSourceContent()); 35 | $cg->setSourceDirty(false); 36 | 37 | $docBlock = $classReflection->getDocBlock(); 38 | 39 | if ($docBlock) { 40 | $cg->setDocBlock(DocBlockGenerator::fromReflection($docBlock)); 41 | } 42 | 43 | // set the namespace 44 | if ($classReflection->inNamespace()) { 45 | $cg->setNamespaceName($classReflection->getNamespaceName()); 46 | } 47 | 48 | foreach ($classReflection->getMethods() as $reflectionMethod) { 49 | $className = $cg->getName(); 50 | $namespaceName = $cg->getNamespaceName(); 51 | if ($namespaceName !== null) { 52 | $className = $namespaceName . '\\' . $className; 53 | } 54 | 55 | if ($reflectionMethod->getDeclaringClass()->getName() == $className) { 56 | $methods[] = MethodGenerator::fromReflection($reflectionMethod); 57 | } 58 | } 59 | 60 | foreach ($classReflection->getConstants() as $name => $value) { 61 | $cg->addConstant($name, $value); 62 | } 63 | 64 | $cg->addMethods($methods); 65 | 66 | return $cg; 67 | } 68 | 69 | /** 70 | * Generate from array 71 | * 72 | * @deprecated this API is deprecated, and will be removed in the next major release. Please 73 | * use the other constructors of this class instead. 74 | * 75 | * @configkey name string [required] Class Name 76 | * @configkey filegenerator FileGenerator File generator that holds this class 77 | * @configkey namespacename string The namespace for this class 78 | * @configkey docblock string The docblock information 79 | * @configkey constants 80 | * @configkey methods 81 | * @throws Exception\InvalidArgumentException 82 | * @return static 83 | */ 84 | public static function fromArray(array $array) 85 | { 86 | if (! isset($array['name'])) { 87 | throw new Exception\InvalidArgumentException( 88 | 'Class generator requires that a name is provided for this object' 89 | ); 90 | } 91 | 92 | $cg = new static($array['name']); 93 | foreach ($array as $name => $value) { 94 | // normalize key 95 | switch (strtolower(str_replace(['.', '-', '_'], '', $name))) { 96 | case 'containingfile': 97 | $cg->setContainingFileGenerator($value); 98 | break; 99 | case 'namespacename': 100 | $cg->setNamespaceName($value); 101 | break; 102 | case 'docblock': 103 | $docBlock = $value instanceof DocBlockGenerator ? $value : DocBlockGenerator::fromArray($value); 104 | $cg->setDocBlock($docBlock); 105 | break; 106 | case 'methods': 107 | $cg->addMethods($value); 108 | break; 109 | case 'constants': 110 | $cg->addConstants($value); 111 | break; 112 | } 113 | } 114 | 115 | return $cg; 116 | } 117 | 118 | /** @inheritDoc */ 119 | public function addPropertyFromGenerator(PropertyGenerator $property) 120 | { 121 | return $this; 122 | } 123 | 124 | /** @inheritDoc */ 125 | public function addMethodFromGenerator(MethodGenerator $method) 126 | { 127 | $method->setInterface(true); 128 | 129 | return parent::addMethodFromGenerator($method); 130 | } 131 | 132 | /** @inheritDoc */ 133 | public function setExtendedClass($extendedClass) 134 | { 135 | return $this; 136 | } 137 | 138 | /** @inheritDoc */ 139 | public function setAbstract($isAbstract) 140 | { 141 | return $this; 142 | } 143 | } 144 | -------------------------------------------------------------------------------- /src/Generator/MethodGenerator.php: -------------------------------------------------------------------------------- 1 | setSourceContent($reflectionMethod->getContents(false)); 44 | $method->setSourceDirty(false); 45 | 46 | if ($reflectionMethod->getDocComment() != '') { 47 | $method->setDocBlock(DocBlockGenerator::fromReflection($reflectionMethod->getDocBlock())); 48 | } 49 | 50 | $method->setBody(static::clearBodyIndention($reflectionMethod->getBody())); 51 | 52 | return $method; 53 | } 54 | 55 | /** 56 | * Returns a MethodGenerator based on a MethodReflection with only the signature copied. 57 | * 58 | * This is similar to fromReflection() but without the method body and phpdoc as this is quite heavy to copy. 59 | * It's for example useful when creating proxies where you normally change the method body anyway. 60 | */ 61 | public static function copyMethodSignature(MethodReflection $reflectionMethod): MethodGenerator 62 | { 63 | $method = new static(); 64 | $declaringClass = $reflectionMethod->getDeclaringClass(); 65 | 66 | $method->returnType = TypeGenerator::fromReflectionType($reflectionMethod->getReturnType(), $declaringClass); 67 | $method->setFinal($reflectionMethod->isFinal()); 68 | 69 | if ($reflectionMethod->isPrivate()) { 70 | $method->setVisibility(self::VISIBILITY_PRIVATE); 71 | } elseif ($reflectionMethod->isProtected()) { 72 | $method->setVisibility(self::VISIBILITY_PROTECTED); 73 | } else { 74 | $method->setVisibility(self::VISIBILITY_PUBLIC); 75 | } 76 | 77 | $method->setInterface($declaringClass->isInterface()); 78 | $method->setStatic($reflectionMethod->isStatic()); 79 | $method->setReturnsReference($reflectionMethod->returnsReference()); 80 | $method->setName($reflectionMethod->getName()); 81 | 82 | foreach ($reflectionMethod->getParameters() as $reflectionParameter) { 83 | $method->setParameter( 84 | $reflectionParameter->isPromoted() 85 | ? PromotedParameterGenerator::fromReflection($reflectionParameter) 86 | : ParameterGenerator::fromReflection($reflectionParameter) 87 | ); 88 | } 89 | 90 | return $method; 91 | } 92 | 93 | /** 94 | * Identify the space indention from the first line and remove this indention 95 | * from all lines 96 | * 97 | * @param string $body 98 | * @return string 99 | */ 100 | protected static function clearBodyIndention($body) 101 | { 102 | if (empty($body)) { 103 | return $body; 104 | } 105 | 106 | $lines = explode("\n", $body); 107 | 108 | $indention = str_replace(trim($lines[1]), '', $lines[1]); 109 | 110 | foreach ($lines as $key => $line) { 111 | if (str_starts_with($line, $indention)) { 112 | $lines[$key] = substr($line, strlen($indention)); 113 | } 114 | } 115 | 116 | $body = implode("\n", $lines); 117 | 118 | return $body; 119 | } 120 | 121 | /** 122 | * Generate from array 123 | * 124 | * @deprecated this API is deprecated, and will be removed in the next major release. Please 125 | * use the other constructors of this class instead. 126 | * 127 | * @configkey name string [required] Class Name 128 | * @configkey docblock string The DocBlock information 129 | * @configkey flags int Flags, one of self::FLAG_ABSTRACT, self::FLAG_FINAL 130 | * @configkey parameters string Class which this class is extending 131 | * @configkey body string 132 | * @configkey returntype string 133 | * @configkey returnsreference bool 134 | * @configkey abstract bool 135 | * @configkey final bool 136 | * @configkey static bool 137 | * @configkey visibility string 138 | * @throws Exception\InvalidArgumentException 139 | * @return MethodGenerator 140 | */ 141 | public static function fromArray(array $array) 142 | { 143 | if (! isset($array['name'])) { 144 | throw new Exception\InvalidArgumentException( 145 | 'Method generator requires that a name is provided for this object' 146 | ); 147 | } 148 | 149 | $method = new static($array['name']); 150 | foreach ($array as $name => $value) { 151 | // normalize key 152 | switch (strtolower(str_replace(['.', '-', '_'], '', $name))) { 153 | case 'docblock': 154 | $docBlock = $value instanceof DocBlockGenerator ? $value : DocBlockGenerator::fromArray($value); 155 | $method->setDocBlock($docBlock); 156 | break; 157 | case 'flags': 158 | $method->setFlags($value); 159 | break; 160 | case 'parameters': 161 | $method->setParameters($value); 162 | break; 163 | case 'body': 164 | $method->setBody($value); 165 | break; 166 | case 'abstract': 167 | $method->setAbstract($value); 168 | break; 169 | case 'final': 170 | $method->setFinal($value); 171 | break; 172 | case 'interface': 173 | $method->setInterface($value); 174 | break; 175 | case 'static': 176 | $method->setStatic($value); 177 | break; 178 | case 'visibility': 179 | $method->setVisibility($value); 180 | break; 181 | case 'returntype': 182 | $method->setReturnType($value); 183 | break; 184 | case 'returnsreference': 185 | $method->setReturnsReference((bool) $value); 186 | } 187 | } 188 | 189 | return $method; 190 | } 191 | 192 | /** 193 | * @param ?string $name 194 | * @param ParameterGenerator[]|array[]|string[] $parameters 195 | * @param int|int[] $flags 196 | * @param ?string $body 197 | * @param DocBlockGenerator|string|null $docBlock 198 | */ 199 | public function __construct( 200 | $name = null, 201 | array $parameters = [], 202 | $flags = self::FLAG_PUBLIC, 203 | $body = null, 204 | $docBlock = null 205 | ) { 206 | if ($name) { 207 | $this->setName($name); 208 | } 209 | if ($parameters) { 210 | $this->setParameters($parameters); 211 | } 212 | if ($flags !== self::FLAG_PUBLIC) { 213 | $this->setFlags($flags); 214 | } 215 | if ($body) { 216 | $this->setBody($body); 217 | } 218 | if ($docBlock) { 219 | $this->setDocBlock($docBlock); 220 | } 221 | } 222 | 223 | /** 224 | * @param ParameterGenerator[]|array[]|string[] $parameters 225 | * @return MethodGenerator 226 | */ 227 | public function setParameters(array $parameters) 228 | { 229 | foreach ($parameters as $parameter) { 230 | $this->setParameter($parameter); 231 | } 232 | 233 | $this->sortParameters(); 234 | 235 | return $this; 236 | } 237 | 238 | /** 239 | * @param ParameterGenerator|array|string $parameter 240 | * @throws Exception\InvalidArgumentException 241 | * @return MethodGenerator 242 | */ 243 | public function setParameter($parameter) 244 | { 245 | if (is_string($parameter)) { 246 | $parameter = new ParameterGenerator($parameter); 247 | } 248 | 249 | if (is_array($parameter)) { 250 | $parameter = ParameterGenerator::fromArray($parameter); 251 | } 252 | 253 | if (! $parameter instanceof ParameterGenerator) { 254 | throw new Exception\InvalidArgumentException(sprintf( 255 | '%s is expecting either a string, array or an instance of %s\ParameterGenerator', 256 | __METHOD__, 257 | __NAMESPACE__ 258 | )); 259 | } 260 | 261 | $this->parameters[$parameter->getName()] = $parameter; 262 | 263 | $this->sortParameters(); 264 | 265 | return $this; 266 | } 267 | 268 | /** 269 | * @return ParameterGenerator[] 270 | */ 271 | public function getParameters() 272 | { 273 | return $this->parameters; 274 | } 275 | 276 | /** 277 | * @param string $body 278 | * @return MethodGenerator 279 | */ 280 | public function setBody($body) 281 | { 282 | $this->body = $body; 283 | return $this; 284 | } 285 | 286 | /** 287 | * @return string 288 | */ 289 | public function getBody() 290 | { 291 | return $this->body; 292 | } 293 | 294 | /** 295 | * @param string|null $returnType 296 | * @return MethodGenerator 297 | */ 298 | public function setReturnType($returnType = null) 299 | { 300 | $this->returnType = null === $returnType 301 | ? null 302 | : TypeGenerator::fromTypeString($returnType); 303 | 304 | return $this; 305 | } 306 | 307 | /** 308 | * @return TypeGenerator|null 309 | */ 310 | public function getReturnType() 311 | { 312 | return $this->returnType; 313 | } 314 | 315 | /** 316 | * @param bool $returnsReference 317 | * @return MethodGenerator 318 | */ 319 | public function setReturnsReference($returnsReference) 320 | { 321 | $this->returnsReference = (bool) $returnsReference; 322 | 323 | return $this; 324 | } 325 | 326 | public function returnsReference(): bool 327 | { 328 | return $this->returnsReference; 329 | } 330 | 331 | /** 332 | * Sort parameters by their position 333 | */ 334 | private function sortParameters(): void 335 | { 336 | uasort( 337 | $this->parameters, 338 | static fn(ParameterGenerator $item1, ParameterGenerator $item2) 339 | => $item1->getPosition() <=> $item2->getPosition() 340 | ); 341 | } 342 | 343 | /** 344 | * @return string 345 | */ 346 | public function generate() 347 | { 348 | $output = ''; 349 | 350 | $indent = $this->getIndentation(); 351 | 352 | if (($docBlock = $this->getDocBlock()) !== null) { 353 | $docBlock->setIndentation($indent); 354 | $output .= $docBlock->generate(); 355 | } 356 | 357 | $output .= $indent; 358 | 359 | if ($this->isAbstract()) { 360 | $output .= 'abstract '; 361 | } else { 362 | $output .= $this->isFinal() ? 'final ' : ''; 363 | } 364 | 365 | $output .= $this->getVisibility() 366 | . ($this->isStatic() ? ' static' : '') 367 | . ' function ' 368 | . ($this->returnsReference ? '& ' : '') 369 | . $this->getName() . '('; 370 | 371 | $output .= implode(', ', array_map( 372 | static fn (ParameterGenerator $parameter): string => $parameter->generate(), 373 | $this->getParameters() 374 | )); 375 | 376 | $output .= ')'; 377 | 378 | if ($this->returnType) { 379 | $output .= ' : ' . $this->returnType->generate(); 380 | } 381 | 382 | if ($this->isAbstract()) { 383 | return $output . ';'; 384 | } 385 | 386 | if ($this->isInterface()) { 387 | return $output . ';'; 388 | } 389 | 390 | $output .= self::LINE_FEED . $indent . '{' . self::LINE_FEED; 391 | 392 | if ($this->body) { 393 | $output .= preg_replace('#^((?![a-zA-Z0-9_-]+;).+?)$#m', $indent . $indent . '$1', trim($this->body)) 394 | . self::LINE_FEED; 395 | } 396 | 397 | $output .= $indent . '}' . self::LINE_FEED; 398 | 399 | return $output; 400 | } 401 | 402 | public function __toString(): string 403 | { 404 | return $this->generate(); 405 | } 406 | } 407 | -------------------------------------------------------------------------------- /src/Generator/ParameterGenerator.php: -------------------------------------------------------------------------------- 1 | setName($reflectionParameter->getName()); 35 | $param->type = TypeGenerator::fromReflectionType( 36 | $reflectionParameter->getType(), 37 | $reflectionParameter->getDeclaringClass() 38 | ); 39 | 40 | $param->setPosition($reflectionParameter->getPosition()); 41 | 42 | $variadic = $reflectionParameter->isVariadic(); 43 | 44 | $param->setVariadic($variadic); 45 | 46 | if (! $variadic && ($reflectionParameter->isOptional() || $reflectionParameter->isDefaultValueAvailable())) { 47 | try { 48 | $param->setDefaultValue($reflectionParameter->getDefaultValue()); 49 | } catch (ReflectionException) { 50 | $param->setDefaultValue(null); 51 | } 52 | } 53 | 54 | $param->setPassedByReference($reflectionParameter->isPassedByReference()); 55 | 56 | return $param; 57 | } 58 | 59 | /** 60 | * Generate from array 61 | * 62 | * @deprecated this API is deprecated, and will be removed in the next major release. Please 63 | * use the other constructors of this class instead. 64 | * 65 | * @configkey name string [required] Class Name 66 | * @configkey type string 67 | * @configkey defaultvalue null|bool|string|int|float|array|ValueGenerator 68 | * @configkey passedbyreference bool 69 | * @configkey position int 70 | * @configkey sourcedirty bool 71 | * @configkey indentation string 72 | * @configkey sourcecontent string 73 | * @configkey omitdefaultvalue bool 74 | * @throws Exception\InvalidArgumentException 75 | * @return ParameterGenerator 76 | */ 77 | public static function fromArray(array $array) 78 | { 79 | if (! isset($array['name'])) { 80 | throw new Exception\InvalidArgumentException( 81 | 'Parameter generator requires that a name is provided for this object' 82 | ); 83 | } 84 | 85 | $param = new static($array['name']); 86 | foreach ($array as $name => $value) { 87 | // normalize key 88 | switch (strtolower(str_replace(['.', '-', '_'], '', $name))) { 89 | case 'type': 90 | $param->setType($value); 91 | break; 92 | case 'defaultvalue': 93 | $param->setDefaultValue($value); 94 | break; 95 | case 'passedbyreference': 96 | $param->setPassedByReference($value); 97 | break; 98 | case 'position': 99 | $param->setPosition($value); 100 | break; 101 | case 'sourcedirty': 102 | $param->setSourceDirty($value); 103 | break; 104 | case 'indentation': 105 | $param->setIndentation($value); 106 | break; 107 | case 'sourcecontent': 108 | $param->setSourceContent($value); 109 | break; 110 | case 'omitdefaultvalue': 111 | $param->omitDefaultValue($value); 112 | break; 113 | } 114 | } 115 | 116 | return $param; 117 | } 118 | 119 | /** 120 | * @param ?string $name 121 | * @param ?string $type 122 | * @param mixed $defaultValue 123 | * @param ?int $position 124 | * @param bool $passByReference 125 | */ 126 | public function __construct( 127 | $name = null, 128 | $type = null, 129 | $defaultValue = null, 130 | $position = null, 131 | $passByReference = false 132 | ) { 133 | if (null !== $name) { 134 | $this->setName($name); 135 | } 136 | if (null !== $type) { 137 | $this->setType($type); 138 | } 139 | if (null !== $defaultValue) { 140 | $this->setDefaultValue($defaultValue); 141 | } 142 | if (null !== $position) { 143 | $this->setPosition($position); 144 | } 145 | if (false !== $passByReference) { 146 | $this->setPassedByReference(true); 147 | } 148 | } 149 | 150 | /** 151 | * @param string $type 152 | * @return ParameterGenerator 153 | */ 154 | public function setType($type) 155 | { 156 | $this->type = TypeGenerator::fromTypeString($type); 157 | 158 | return $this; 159 | } 160 | 161 | /** @return string|null */ 162 | public function getType() 163 | { 164 | return $this->type 165 | ? $this->type->__toString() 166 | : null; 167 | } 168 | 169 | /** 170 | * @param string $name 171 | * @return ParameterGenerator 172 | */ 173 | public function setName($name) 174 | { 175 | $this->name = (string) $name; 176 | return $this; 177 | } 178 | 179 | /** 180 | * @return string 181 | */ 182 | public function getName() 183 | { 184 | return $this->name; 185 | } 186 | 187 | /** 188 | * Set the default value of the parameter. 189 | * 190 | * Certain variables are difficult to express 191 | * 192 | * @param mixed $defaultValue 193 | * @return ParameterGenerator 194 | */ 195 | public function setDefaultValue($defaultValue) 196 | { 197 | if ($this->variadic) { 198 | throw new Exception\InvalidArgumentException('Variadic parameter cannot have a default value'); 199 | } 200 | 201 | $this->defaultValue = $defaultValue instanceof ValueGenerator 202 | ? $defaultValue 203 | : new ValueGenerator($defaultValue); 204 | 205 | return $this; 206 | } 207 | 208 | /** 209 | * @return ?ValueGenerator 210 | */ 211 | public function getDefaultValue() 212 | { 213 | return $this->defaultValue; 214 | } 215 | 216 | /** 217 | * @param int $position 218 | * @return ParameterGenerator 219 | */ 220 | public function setPosition($position) 221 | { 222 | $this->position = (int) $position; 223 | return $this; 224 | } 225 | 226 | /** 227 | * @return int 228 | */ 229 | public function getPosition() 230 | { 231 | return $this->position; 232 | } 233 | 234 | /** 235 | * @return bool 236 | */ 237 | public function getPassedByReference() 238 | { 239 | return $this->passedByReference; 240 | } 241 | 242 | /** 243 | * @param bool $passedByReference 244 | * @return ParameterGenerator 245 | */ 246 | public function setPassedByReference($passedByReference) 247 | { 248 | $this->passedByReference = (bool) $passedByReference; 249 | return $this; 250 | } 251 | 252 | /** 253 | * @param bool $variadic 254 | * @return ParameterGenerator 255 | */ 256 | public function setVariadic($variadic) 257 | { 258 | $this->variadic = (bool) $variadic; 259 | 260 | if (true === $this->variadic && isset($this->defaultValue)) { 261 | throw new Exception\InvalidArgumentException('Variadic parameter cannot have a default value'); 262 | } 263 | 264 | return $this; 265 | } 266 | 267 | /** 268 | * @return bool 269 | */ 270 | public function getVariadic() 271 | { 272 | return $this->variadic; 273 | } 274 | 275 | /** 276 | * @return string 277 | */ 278 | public function generate() 279 | { 280 | $output = $this->generateTypeHint(); 281 | 282 | if (true === $this->passedByReference) { 283 | $output .= '&'; 284 | } 285 | 286 | if ($this->variadic) { 287 | $output .= '... '; 288 | } 289 | 290 | $output .= '$' . $this->name; 291 | 292 | if ($this->omitDefaultValue) { 293 | return $output; 294 | } 295 | 296 | if ($this->defaultValue instanceof ValueGenerator) { 297 | $output .= ' = '; 298 | $this->defaultValue->setOutputMode(ValueGenerator::OUTPUT_SINGLE_LINE); 299 | $output .= $this->defaultValue; 300 | } 301 | 302 | return $output; 303 | } 304 | 305 | /** 306 | * @return string 307 | */ 308 | private function generateTypeHint() 309 | { 310 | if (null === $this->type) { 311 | return ''; 312 | } 313 | 314 | return $this->type->generate() . ' '; 315 | } 316 | 317 | /** 318 | * @return ParameterGenerator 319 | */ 320 | public function omitDefaultValue(bool $omit = true) 321 | { 322 | $this->omitDefaultValue = $omit; 323 | 324 | return $this; 325 | } 326 | } 327 | -------------------------------------------------------------------------------- /src/Generator/PromotedParameterGenerator.php: -------------------------------------------------------------------------------- 1 | visibility . ' ' . parent::generate(); 43 | } 44 | 45 | public static function fromReflection(ParameterReflection $reflectionParameter): self 46 | { 47 | if (! $reflectionParameter->isPromoted()) { 48 | throw new RuntimeException( 49 | sprintf('Can not create "%s" from unprompted reflection.', self::class) 50 | ); 51 | } 52 | 53 | $visibility = self::VISIBILITY_PUBLIC; 54 | 55 | if ($reflectionParameter->isProtectedPromoted()) { 56 | $visibility = self::VISIBILITY_PROTECTED; 57 | } elseif ($reflectionParameter->isPrivatePromoted()) { 58 | $visibility = self::VISIBILITY_PRIVATE; 59 | } 60 | 61 | return self::fromParameterGeneratorWithVisibility( 62 | parent::fromReflection($reflectionParameter), 63 | $visibility 64 | ); 65 | } 66 | 67 | /** @psalm-param PromotedParameterGenerator::VISIBILITY_* $visibility */ 68 | public static function fromParameterGeneratorWithVisibility(ParameterGenerator $generator, string $visibility): self 69 | { 70 | $name = $generator->getName(); 71 | $type = $generator->getType(); 72 | 73 | if ('' === $name) { 74 | throw new \Laminas\Code\Generator\Exception\RuntimeException( 75 | 'Name of promoted parameter must be non-empty-string.' 76 | ); 77 | } 78 | 79 | if ('' === $type) { 80 | throw new \Laminas\Code\Generator\Exception\RuntimeException( 81 | 'Type of promoted parameter must be non-empty-string.' 82 | ); 83 | } 84 | 85 | return new self( 86 | $name, 87 | $type, 88 | $visibility, 89 | $generator->getPosition(), 90 | $generator->getPassedByReference() 91 | ); 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /src/Generator/PropertyGenerator.php: -------------------------------------------------------------------------------- 1 | setName($name); 39 | } 40 | if (null !== $defaultValue) { 41 | $this->setDefaultValue($defaultValue); 42 | } 43 | if ($flags !== self::FLAG_PUBLIC) { 44 | $this->setFlags($flags); 45 | } 46 | } 47 | 48 | /** @return static */ 49 | public static function fromReflection(PropertyReflection $reflectionProperty) 50 | { 51 | $property = new static(); 52 | 53 | $property->setName($reflectionProperty->getName()); 54 | 55 | $allDefaultProperties = $reflectionProperty->getDeclaringClass()->getDefaultProperties(); 56 | 57 | $defaultValue = $allDefaultProperties[$reflectionProperty->getName()] ?? null; 58 | $property->setDefaultValue($defaultValue); 59 | if ($defaultValue === null) { 60 | $property->omitDefaultValue = true; 61 | } 62 | 63 | $docBlock = $reflectionProperty->getDocBlock(); 64 | 65 | if ($docBlock) { 66 | $property->setDocBlock(DocBlockGenerator::fromReflection($docBlock)); 67 | } 68 | 69 | if ($reflectionProperty->isStatic()) { 70 | $property->setStatic(true); 71 | } 72 | 73 | if ($reflectionProperty->isReadonly()) { 74 | $property->setReadonly(true); 75 | } 76 | 77 | if ($reflectionProperty->isPrivate()) { 78 | $property->setVisibility(self::VISIBILITY_PRIVATE); 79 | } elseif ($reflectionProperty->isProtected()) { 80 | $property->setVisibility(self::VISIBILITY_PROTECTED); 81 | } else { 82 | $property->setVisibility(self::VISIBILITY_PUBLIC); 83 | } 84 | 85 | $property->setType(TypeGenerator::fromReflectionType( 86 | $reflectionProperty->getType(), 87 | $reflectionProperty->getDeclaringClass() 88 | )); 89 | 90 | $property->setSourceDirty(false); 91 | 92 | return $property; 93 | } 94 | 95 | /** 96 | * Generate from array 97 | * 98 | * @deprecated this API is deprecated, and will be removed in the next major release. Please 99 | * use the other constructors of this class instead. 100 | * 101 | * @configkey name string [required] Class Name 102 | * @configkey const bool 103 | * @configkey defaultvalue null|bool|string|int|float|array|ValueGenerator 104 | * @configkey flags int 105 | * @configkey abstract bool 106 | * @configkey final bool 107 | * @configkey static bool 108 | * @configkey visibility string 109 | * @configkey omitdefaultvalue bool 110 | * @configkey readonly bool 111 | * @configkey type null|TypeGenerator 112 | * @return static 113 | * @throws Exception\InvalidArgumentException 114 | */ 115 | public static function fromArray(array $array) 116 | { 117 | if (! isset($array['name'])) { 118 | throw new Exception\InvalidArgumentException( 119 | 'Property generator requires that a name is provided for this object' 120 | ); 121 | } 122 | 123 | $property = new static($array['name']); 124 | foreach ($array as $name => $value) { 125 | // normalize key 126 | switch (strtolower(str_replace(['.', '-', '_'], '', $name))) { 127 | case 'const': 128 | $property->setConst($value); 129 | break; 130 | case 'defaultvalue': 131 | $property->setDefaultValue($value); 132 | break; 133 | case 'docblock': 134 | $docBlock = $value instanceof DocBlockGenerator ? $value : DocBlockGenerator::fromArray($value); 135 | $property->setDocBlock($docBlock); 136 | break; 137 | case 'flags': 138 | $property->setFlags($value); 139 | break; 140 | case 'abstract': 141 | $property->setAbstract($value); 142 | break; 143 | case 'final': 144 | $property->setFinal($value); 145 | break; 146 | case 'static': 147 | $property->setStatic($value); 148 | break; 149 | case 'visibility': 150 | $property->setVisibility($value); 151 | break; 152 | case 'omitdefaultvalue': 153 | $property->omitDefaultValue($value); 154 | break; 155 | case 'readonly': 156 | if (! is_bool($value)) { 157 | throw new Exception\InvalidArgumentException(sprintf( 158 | '%s is expecting boolean on key %s. Got %s', 159 | __METHOD__, 160 | $name, 161 | get_debug_type($value) 162 | )); 163 | } 164 | 165 | $property->setReadonly($value); 166 | break; 167 | case 'type': 168 | if (! $value instanceof TypeGenerator) { 169 | throw new Exception\InvalidArgumentException(sprintf( 170 | '%s is expecting %s on key %s. Got %s', 171 | __METHOD__, 172 | TypeGenerator::class, 173 | $name, 174 | get_debug_type($value) 175 | )); 176 | } 177 | $property->setType($value); 178 | break; 179 | } 180 | } 181 | 182 | return $property; 183 | } 184 | 185 | /** 186 | * @param bool $const 187 | * @return PropertyGenerator 188 | */ 189 | public function setConst($const) 190 | { 191 | if (true === $const) { 192 | $this->setFlags(self::FLAG_CONSTANT); 193 | 194 | return $this; 195 | } 196 | 197 | $this->removeFlag(self::FLAG_CONSTANT); 198 | 199 | return $this; 200 | } 201 | 202 | /** 203 | * @return bool 204 | */ 205 | public function isConst() 206 | { 207 | return (bool) ($this->flags & self::FLAG_CONSTANT); 208 | } 209 | 210 | public function setReadonly(bool $readonly): self 211 | { 212 | if (true === $readonly) { 213 | $this->setFlags(self::FLAG_READONLY); 214 | 215 | return $this; 216 | } 217 | 218 | $this->removeFlag(self::FLAG_READONLY); 219 | 220 | return $this; 221 | } 222 | 223 | public function isReadonly(): bool 224 | { 225 | return (bool) ($this->flags & self::FLAG_READONLY); 226 | } 227 | 228 | /** @inheritDoc */ 229 | public function setFlags($flags) 230 | { 231 | $flags = array_reduce((array) $flags, static fn(int $a, int $b): int => $a | $b, 0); 232 | 233 | if ($flags & self::FLAG_READONLY && $flags & self::FLAG_STATIC) { 234 | throw new Exception\RuntimeException('Modifier "readonly" in combination with "static" not permitted.'); 235 | } 236 | 237 | if ($flags & self::FLAG_READONLY && $flags & self::FLAG_CONSTANT) { 238 | throw new Exception\RuntimeException('Modifier "readonly" in combination with "constant" not permitted.'); 239 | } 240 | 241 | return parent::setFlags($flags); 242 | } 243 | 244 | /** 245 | * @return ?PropertyValueGenerator 246 | */ 247 | public function getDefaultValue() 248 | { 249 | return $this->defaultValue; 250 | } 251 | 252 | /** 253 | * @param PropertyValueGenerator|mixed $defaultValue 254 | * @param PropertyValueGenerator::TYPE_* $defaultValueType 255 | * @param PropertyValueGenerator::OUTPUT_* $defaultValueOutputMode 256 | * @return static 257 | */ 258 | public function setDefaultValue( 259 | $defaultValue, 260 | $defaultValueType = PropertyValueGenerator::TYPE_AUTO, 261 | $defaultValueOutputMode = PropertyValueGenerator::OUTPUT_MULTIPLE_LINE 262 | ) { 263 | if (! $defaultValue instanceof PropertyValueGenerator) { 264 | $defaultValue = new PropertyValueGenerator($defaultValue, $defaultValueType, $defaultValueOutputMode); 265 | } 266 | 267 | $this->defaultValue = $defaultValue; 268 | 269 | return $this; 270 | } 271 | 272 | /** 273 | * @return string 274 | * @psalm-return non-empty-string 275 | * @throws Exception\RuntimeException 276 | */ 277 | public function generate() 278 | { 279 | $name = $this->getName(); 280 | $defaultValue = $this->getDefaultValue(); 281 | 282 | $output = ''; 283 | 284 | if (($docBlock = $this->getDocBlock()) !== null) { 285 | $docBlock->setIndentation(' '); 286 | $output .= $docBlock->generate(); 287 | } 288 | 289 | if ($this->isConst()) { 290 | if ($defaultValue !== null && ! $defaultValue->isValidConstantType()) { 291 | throw new Exception\RuntimeException(sprintf( 292 | 'The property %s is said to be ' 293 | . 'constant but does not have a valid constant value.', 294 | $this->name 295 | )); 296 | } 297 | 298 | return $output 299 | . $this->indentation 300 | . ($this->isFinal() ? 'final ' : '') 301 | . $this->getVisibility() 302 | . ' const ' 303 | . $name . ' = ' 304 | . ($defaultValue !== null ? $defaultValue->generate() : 'null;'); 305 | } 306 | 307 | $type = $this->type; 308 | $output .= $this->indentation 309 | . $this->getVisibility() 310 | . ($this->isReadonly() ? ' readonly' : '') 311 | . ($this->isStatic() ? ' static' : '') 312 | . ($type ? ' ' . $type->generate() : '') 313 | . ' $' . $name; 314 | 315 | if ($this->omitDefaultValue) { 316 | return $output . ';'; 317 | } 318 | 319 | return $output . ' = ' . ($defaultValue !== null ? $defaultValue->generate() : 'null;'); 320 | } 321 | 322 | /** 323 | * @return PropertyGenerator 324 | */ 325 | public function omitDefaultValue(bool $omit = true) 326 | { 327 | $this->omitDefaultValue = $omit; 328 | 329 | return $this; 330 | } 331 | 332 | public function getType(): ?TypeGenerator 333 | { 334 | return $this->type; 335 | } 336 | 337 | public function setType(?TypeGenerator $type): void 338 | { 339 | $this->type = $type; 340 | } 341 | } 342 | -------------------------------------------------------------------------------- /src/Generator/PropertyValueGenerator.php: -------------------------------------------------------------------------------- 1 | getName()); 23 | 24 | $cg->setSourceContent($cg->getSourceContent()); 25 | $cg->setSourceDirty(false); 26 | 27 | if ($classReflection->getDocComment() != '') { 28 | $cg->setDocBlock(DocBlockGenerator::fromReflection($classReflection->getDocBlock())); 29 | } 30 | 31 | // set the namespace 32 | if ($classReflection->inNamespace()) { 33 | $cg->setNamespaceName($classReflection->getNamespaceName()); 34 | } 35 | 36 | $properties = []; 37 | foreach ($classReflection->getProperties() as $reflectionProperty) { 38 | if ($reflectionProperty->getDeclaringClass()->getName() == $classReflection->getName()) { 39 | $properties[] = PropertyGenerator::fromReflection($reflectionProperty); 40 | } 41 | } 42 | $cg->addProperties($properties); 43 | 44 | $methods = []; 45 | foreach ($classReflection->getMethods() as $reflectionMethod) { 46 | $className = $cg->getName(); 47 | $namespaceName = $cg->getNamespaceName(); 48 | if ($namespaceName !== null) { 49 | $className = $namespaceName . '\\' . $className; 50 | } 51 | if ($reflectionMethod->getDeclaringClass()->getName() == $className) { 52 | $methods[] = MethodGenerator::fromReflection($reflectionMethod); 53 | } 54 | } 55 | $cg->addMethods($methods); 56 | 57 | return $cg; 58 | } 59 | 60 | /** 61 | * Generate from array 62 | * 63 | * @deprecated this API is deprecated, and will be removed in the next major release. Please 64 | * use the other constructors of this class instead. 65 | * 66 | * @configkey name string [required] Class Name 67 | * @configkey filegenerator FileGenerator File generator that holds this class 68 | * @configkey namespacename string The namespace for this class 69 | * @configkey docblock string The docblock information 70 | * @configkey properties 71 | * @configkey methods 72 | * @throws Exception\InvalidArgumentException 73 | * @return static 74 | */ 75 | public static function fromArray(array $array) 76 | { 77 | if (! isset($array['name'])) { 78 | throw new Exception\InvalidArgumentException( 79 | 'Class generator requires that a name is provided for this object' 80 | ); 81 | } 82 | 83 | $cg = new static($array['name']); 84 | foreach ($array as $name => $value) { 85 | // normalize key 86 | switch (strtolower(str_replace(['.', '-', '_'], '', $name))) { 87 | case 'containingfile': 88 | $cg->setContainingFileGenerator($value); 89 | break; 90 | case 'namespacename': 91 | $cg->setNamespaceName($value); 92 | break; 93 | case 'docblock': 94 | $docBlock = $value instanceof DocBlockGenerator ? $value : DocBlockGenerator::fromArray($value); 95 | $cg->setDocBlock($docBlock); 96 | break; 97 | case 'properties': 98 | $cg->addProperties($value); 99 | break; 100 | case 'methods': 101 | $cg->addMethods($value); 102 | break; 103 | } 104 | } 105 | 106 | return $cg; 107 | } 108 | 109 | /** 110 | * @inheritDoc 111 | * @param int[]|int $flags 112 | */ 113 | public function setFlags($flags) 114 | { 115 | return $this; 116 | } 117 | 118 | /** 119 | * @param int $flag 120 | * @return static 121 | */ 122 | public function addFlag($flag) 123 | { 124 | return $this; 125 | } 126 | 127 | /** 128 | * @param int $flag 129 | * @return static 130 | */ 131 | public function removeFlag($flag) 132 | { 133 | return $this; 134 | } 135 | 136 | /** 137 | * @inheritDoc 138 | */ 139 | public function setFinal($isFinal) 140 | { 141 | return $this; 142 | } 143 | 144 | /** 145 | * @param ?string $extendedClass 146 | * @return static 147 | */ 148 | public function setExtendedClass($extendedClass) 149 | { 150 | return $this; 151 | } 152 | 153 | /** 154 | * @inheritDoc 155 | */ 156 | public function setImplementedInterfaces(array $implementedInterfaces) 157 | { 158 | return $this; 159 | } 160 | 161 | /** 162 | * @inheritDoc 163 | */ 164 | public function setAbstract($isAbstract) 165 | { 166 | return $this; 167 | } 168 | } 169 | -------------------------------------------------------------------------------- /src/Generator/TraitUsageGenerator.php: -------------------------------------------------------------------------------- 1 | Array of trait names */ 26 | protected array $traits = []; 27 | /** 28 | * @var array< 29 | * non-empty-string, 30 | * array{ 31 | * alias: string, 32 | * visibility: Visibility|null 33 | * } 34 | * > Array of trait aliases 35 | */ 36 | protected array $traitAliases = []; 37 | 38 | /** @var array Array of trait overrides */ 39 | protected array $traitOverrides = []; 40 | 41 | /** @var array Array of string names */ 42 | protected array $uses = []; 43 | 44 | public function __construct(protected ClassGenerator $classGenerator) 45 | { 46 | } 47 | 48 | /** 49 | * @inheritDoc 50 | */ 51 | public function addUse($use, $useAlias = null) 52 | { 53 | $this->removeUse($use); 54 | 55 | if (! empty($useAlias)) { 56 | $use .= ' as ' . $useAlias; 57 | } 58 | 59 | $this->uses[$use] = $use; 60 | return $this; 61 | } 62 | 63 | /** @inheritDoc */ 64 | public function getUses() 65 | { 66 | return array_values($this->uses); 67 | } 68 | 69 | /** 70 | * @param string $use 71 | * @return bool 72 | */ 73 | public function hasUse($use) 74 | { 75 | foreach ($this->uses as $key => $value) { 76 | $parts = explode(' ', $value); 77 | if ($parts[0] === $use) { 78 | return true; 79 | } 80 | } 81 | 82 | return false; 83 | } 84 | 85 | /** 86 | * @param string $use 87 | * @return bool 88 | */ 89 | public function hasUseAlias($use) 90 | { 91 | foreach ($this->uses as $key => $value) { 92 | $parts = explode(' as ', $value); 93 | if ($parts[0] === $use && count($parts) == 2) { 94 | return true; 95 | } 96 | } 97 | 98 | return false; 99 | } 100 | 101 | /** 102 | * Returns the alias of the provided FQCN 103 | */ 104 | public function getUseAlias(string $use): ?string 105 | { 106 | foreach ($this->uses as $key => $value) { 107 | $parts = explode(' as ', $key); 108 | if ($parts[0] === $use && count($parts) == 2) { 109 | return $parts[1]; 110 | } 111 | } 112 | return null; 113 | } 114 | 115 | /** 116 | * Returns true if the alias is defined in the use list 117 | */ 118 | public function isUseAlias(string $alias): bool 119 | { 120 | foreach ($this->uses as $key => $value) { 121 | $parts = explode(' as ', $key); 122 | if (count($parts) === 2 && $parts[1] === $alias) { 123 | return true; 124 | } 125 | } 126 | return false; 127 | } 128 | 129 | /** 130 | * @param string $use 131 | * @return TraitUsageGenerator 132 | */ 133 | public function removeUse($use) 134 | { 135 | foreach ($this->uses as $key => $value) { 136 | $parts = explode(' ', $value); 137 | if ($parts[0] === $use) { 138 | unset($this->uses[$value]); 139 | } 140 | } 141 | 142 | return $this; 143 | } 144 | 145 | /** 146 | * @param string $use 147 | * @return TraitUsageGenerator 148 | */ 149 | public function removeUseAlias($use) 150 | { 151 | foreach ($this->uses as $key => $value) { 152 | $parts = explode(' as ', $value); 153 | if ($parts[0] === $use && count($parts) == 2) { 154 | unset($this->uses[$value]); 155 | } 156 | } 157 | 158 | return $this; 159 | } 160 | 161 | /** 162 | * @inheritDoc 163 | */ 164 | public function addTrait($trait) 165 | { 166 | if (is_array($trait)) { 167 | if (! array_key_exists('traitName', $trait)) { 168 | throw new Exception\InvalidArgumentException('Missing required value for traitName'); 169 | } 170 | $traitName = $trait['traitName']; 171 | 172 | if (array_key_exists('aliases', $trait)) { 173 | foreach ($trait['aliases'] as $alias) { 174 | $this->addAlias($alias); 175 | } 176 | } 177 | 178 | if (array_key_exists('insteadof', $trait)) { 179 | foreach ($trait['insteadof'] as $insteadof) { 180 | $this->addTraitOverride($insteadof); 181 | } 182 | } 183 | } else { 184 | $traitName = $trait; 185 | } 186 | 187 | if (! $this->hasTrait($traitName)) { 188 | $this->traits[] = $traitName; 189 | } 190 | 191 | return $this; 192 | } 193 | 194 | /** 195 | * @inheritDoc 196 | */ 197 | public function addTraits(array $traits) 198 | { 199 | foreach ($traits as $trait) { 200 | $this->addTrait($trait); 201 | } 202 | 203 | return $this; 204 | } 205 | 206 | /** 207 | * @inheritDoc 208 | */ 209 | public function hasTrait($traitName) 210 | { 211 | return in_array($traitName, $this->traits); 212 | } 213 | 214 | /** 215 | * @inheritDoc 216 | */ 217 | public function getTraits() 218 | { 219 | return $this->traits; 220 | } 221 | 222 | /** 223 | * @inheritDoc 224 | */ 225 | public function removeTrait($traitName) 226 | { 227 | $key = array_search($traitName, $this->traits); 228 | if (false !== $key) { 229 | unset($this->traits[$key]); 230 | } 231 | 232 | return $this; 233 | } 234 | 235 | /** 236 | * @inheritDoc 237 | */ 238 | public function addTraitAlias($method, $alias, $visibility = null) 239 | { 240 | if (is_array($method)) { 241 | if (! array_key_exists('traitName', $method)) { 242 | throw new Exception\InvalidArgumentException('Missing required argument "traitName" for $method'); 243 | } 244 | 245 | if (! array_key_exists('method', $method)) { 246 | throw new Exception\InvalidArgumentException('Missing required argument "method" for $method'); 247 | } 248 | 249 | $traitAndMethod = $method['traitName'] . '::' . $method['method']; 250 | } else { 251 | $traitAndMethod = $method; 252 | } 253 | 254 | // Validations 255 | if (! str_contains($traitAndMethod, '::')) { 256 | throw new Exception\InvalidArgumentException( 257 | 'Invalid Format: $method must be in the format of trait::method' 258 | ); 259 | } 260 | if (! is_string($alias)) { 261 | throw new Exception\InvalidArgumentException('Invalid Alias: $alias must be a string or array.'); 262 | } 263 | if ($this->classGenerator->hasMethod($alias)) { 264 | throw new Exception\InvalidArgumentException('Invalid Alias: Method name already exists on this class.'); 265 | } 266 | if ( 267 | null !== $visibility 268 | && $visibility !== ReflectionMethod::IS_PUBLIC 269 | && $visibility !== ReflectionMethod::IS_PRIVATE 270 | && $visibility !== ReflectionMethod::IS_PROTECTED 271 | ) { 272 | throw new Exception\InvalidArgumentException( 273 | 'Invalid Type: $visibility must of ReflectionMethod::IS_PUBLIC,' 274 | . ' ReflectionMethod::IS_PRIVATE or ReflectionMethod::IS_PROTECTED' 275 | ); 276 | } 277 | 278 | [$trait, $method] = explode('::', $traitAndMethod); 279 | if (! $this->hasTrait($trait)) { 280 | throw new Exception\InvalidArgumentException('Invalid trait: Trait does not exists on this class'); 281 | } 282 | 283 | $this->traitAliases[$traitAndMethod] = [ 284 | 'alias' => $alias, 285 | 'visibility' => $visibility, 286 | ]; 287 | 288 | return $this; 289 | } 290 | 291 | /** 292 | * @inheritDoc 293 | */ 294 | public function getTraitAliases() 295 | { 296 | return $this->traitAliases; 297 | } 298 | 299 | /** 300 | * @inheritDoc 301 | */ 302 | public function addTraitOverride($method, $traitsToReplace) 303 | { 304 | if (false === is_array($traitsToReplace)) { 305 | $traitsToReplace = [$traitsToReplace]; 306 | } 307 | 308 | $traitAndMethod = $method; 309 | if (is_array($method)) { 310 | if (! array_key_exists('traitName', $method)) { 311 | throw new Exception\InvalidArgumentException('Missing required argument "traitName" for $method'); 312 | } 313 | 314 | if (! array_key_exists('method', $method)) { 315 | throw new Exception\InvalidArgumentException('Missing required argument "method" for $method'); 316 | } 317 | 318 | $traitAndMethod = (string) $method['traitName'] . '::' . (string) $method['method']; 319 | } 320 | 321 | // Validations 322 | if (! str_contains($traitAndMethod, '::')) { 323 | throw new Exception\InvalidArgumentException( 324 | 'Invalid Format: $method must be in the format of trait::method' 325 | ); 326 | } 327 | 328 | [$trait, $method] = explode('::', $traitAndMethod); 329 | if (! $this->hasTrait($trait)) { 330 | throw new Exception\InvalidArgumentException('Invalid trait: Trait does not exists on this class'); 331 | } 332 | 333 | if (! array_key_exists($traitAndMethod, $this->traitOverrides)) { 334 | $this->traitOverrides[$traitAndMethod] = []; 335 | } 336 | 337 | foreach ($traitsToReplace as $traitToReplace) { 338 | if (! is_string($traitToReplace)) { 339 | throw new Exception\InvalidArgumentException( 340 | 'Invalid Argument: $traitToReplace must be a string or array of strings' 341 | ); 342 | } 343 | 344 | if (! in_array($traitToReplace, $this->traitOverrides[$traitAndMethod])) { 345 | $this->traitOverrides[$traitAndMethod][] = $traitToReplace; 346 | } 347 | } 348 | 349 | return $this; 350 | } 351 | 352 | /** 353 | * @inheritDoc 354 | */ 355 | public function removeTraitOverride($method, $overridesToRemove = null) 356 | { 357 | if (! array_key_exists($method, $this->traitOverrides)) { 358 | return $this; 359 | } 360 | 361 | if (null === $overridesToRemove) { 362 | unset($this->traitOverrides[$method]); 363 | return $this; 364 | } 365 | 366 | $overridesToRemove = ! is_array($overridesToRemove) 367 | ? [$overridesToRemove] 368 | : $overridesToRemove; 369 | foreach ($overridesToRemove as $traitToRemove) { 370 | $key = array_search($traitToRemove, $this->traitOverrides[$method]); 371 | if (false !== $key) { 372 | unset($this->traitOverrides[$method][$key]); 373 | } 374 | } 375 | return $this; 376 | } 377 | 378 | /** 379 | * @inheritDoc 380 | */ 381 | public function getTraitOverrides() 382 | { 383 | return $this->traitOverrides; 384 | } 385 | 386 | /** 387 | * @inheritDoc 388 | */ 389 | public function generate() 390 | { 391 | $output = ''; 392 | $indent = $this->getIndentation(); 393 | $traits = $this->getTraits(); 394 | 395 | if (empty($traits)) { 396 | return $output; 397 | } 398 | 399 | $output .= $indent . 'use ' . implode(', ', $traits); 400 | 401 | $aliases = $this->getTraitAliases(); 402 | $overrides = $this->getTraitOverrides(); 403 | if (empty($aliases) && empty($overrides)) { 404 | return $output . ';' . self::LINE_FEED . self::LINE_FEED; 405 | } 406 | 407 | $output .= ' {' . self::LINE_FEED; 408 | foreach ($aliases as $method => $alias) { 409 | $visibility = null !== $alias['visibility'] 410 | ? current(Reflection::getModifierNames($alias['visibility'])) . ' ' 411 | : ''; 412 | 413 | // validation check 414 | if ($this->classGenerator->hasMethod($alias['alias'])) { 415 | throw new Exception\RuntimeException(sprintf( 416 | 'Generation Error: Aliased method %s already exists on this class', 417 | $alias['alias'] 418 | )); 419 | } 420 | 421 | $output .= 422 | $indent 423 | . $indent 424 | . $method 425 | . ' as ' 426 | . $visibility 427 | . $alias['alias'] 428 | . ';' 429 | . self::LINE_FEED; 430 | } 431 | 432 | foreach ($overrides as $method => $insteadofTraits) { 433 | foreach ($insteadofTraits as $insteadofTrait) { 434 | $output .= 435 | $indent 436 | . $indent 437 | . $method 438 | . ' insteadof ' 439 | . $insteadofTrait 440 | . ';' 441 | . self::LINE_FEED; 442 | } 443 | } 444 | 445 | return $output . $indent . '}' . self::LINE_FEED . self::LINE_FEED; 446 | } 447 | } 448 | -------------------------------------------------------------------------------- /src/Generator/TraitUsageInterface.php: -------------------------------------------------------------------------------- 1 | 22 | */ 23 | public function getUses(); 24 | 25 | /** 26 | * Add trait takes an array of trait options or string as arguments. 27 | * 28 | * Array Format: 29 | * key: traitName value: String 30 | * 31 | * key: aliases value: array of arrays 32 | * key: method value: @see addTraitAlias 33 | * key: alias value: @see addTraitAlias 34 | * key: visibility value: @see addTraitAlias 35 | * 36 | * key: insteadof value: array of arrays 37 | * key: method value: @see self::addTraitOverride 38 | * key: traitToReplace value: @see self::addTraitOverride 39 | * 40 | * @param string|array $trait 41 | * @psalm-param string|array{traitName: string, aliases?: array, insteadof?: array} $trait 42 | * @return self 43 | */ 44 | public function addTrait($trait); 45 | 46 | /** 47 | * Add multiple traits. Trait can be an array of trait names or array of trait 48 | * configurations 49 | * 50 | * @param array $traits Array of string names or configurations (@see addTrait) 51 | * @psalm-param list $traits 52 | * @return self 53 | */ 54 | public function addTraits(array $traits); 55 | 56 | /** 57 | * Check to see if the class has a trait defined 58 | * 59 | * @param string $traitName 60 | * @return bool 61 | */ 62 | public function hasTrait($traitName); 63 | 64 | /** 65 | * Get a list of trait names 66 | * 67 | * @return array 68 | */ 69 | public function getTraits(); 70 | 71 | /** 72 | * Remove a trait by its name 73 | * 74 | * @param string $traitName 75 | * @return self 76 | */ 77 | public function removeTrait($traitName); 78 | 79 | /** 80 | * Add a trait alias. This will be used to generate the AS portion of the use statement. 81 | * 82 | * $method: 83 | * This method provides 2 ways for defining the trait method. 84 | * Option 1: String 85 | * Option 2: Array 86 | * key: traitName value: name of trait 87 | * key: method value: trait method 88 | * 89 | * $alias: 90 | * Alias is a string representing the new method name. 91 | * 92 | * @param array{traitName: non-empty-string, method: non-empty-string}|non-empty-string $method 93 | * @param non-empty-string $alias 94 | * @param ReflectionMethod::IS_PUBLIC|ReflectionMethod::IS_PRIVATE|ReflectionMethod::IS_PROTECTED|null $visibility 95 | * @return $this 96 | */ 97 | public function addTraitAlias($method, $alias, $visibility = null); 98 | 99 | /** 100 | * @return array< 101 | * non-empty-string, 102 | * array{ 103 | * alias: string, 104 | * visibility: ReflectionMethod::IS_PRIVATE|ReflectionMethod::IS_PROTECTED|ReflectionMethod::IS_PUBLIC|null 105 | * } 106 | * > 107 | */ 108 | public function getTraitAliases(); 109 | 110 | /** 111 | * Add a trait method override. This will be used to generate the INSTEADOF portion of the use 112 | * statement. 113 | * 114 | * $method: 115 | * This method provides 2 ways for defining the trait method. 116 | * Option 1: String Format: :: 117 | * Option 2: Array 118 | * key: traitName value: trait name 119 | * key: method value: method name 120 | * 121 | * $traitToReplace: 122 | * The name of the trait that you wish to supersede. 123 | * 124 | * This method provides 2 ways for defining the trait method. 125 | * Option 1: String of trait to replace 126 | * Option 2: Array of strings of traits to replace 127 | 128 | * @param mixed $method 129 | * @param mixed $traitsToReplace 130 | * @return $this 131 | */ 132 | public function addTraitOverride($method, $traitsToReplace); 133 | 134 | /** 135 | * Remove an override for a given trait::method 136 | * 137 | * $method: 138 | * This method provides 2 ways for defining the trait method. 139 | * Option 1: String Format: :: 140 | * Option 2: Array 141 | * key: traitName value: trait name 142 | * key: method value: method name 143 | * 144 | * $overridesToRemove: 145 | * The name of the trait that you wish to remove. 146 | * 147 | * This method provides 2 ways for defining the trait method. 148 | * Option 1: String of trait to replace 149 | * Option 2: Array of strings of traits to replace 150 | * 151 | * @param mixed $method 152 | * @param mixed $overridesToRemove 153 | * @return self 154 | */ 155 | public function removeTraitOverride($method, $overridesToRemove = null); 156 | 157 | /** 158 | * Return trait overrides 159 | * 160 | * @return array 161 | */ 162 | public function getTraitOverrides(); 163 | } 164 | -------------------------------------------------------------------------------- /src/Generator/TypeGenerator.php: -------------------------------------------------------------------------------- 1 | assertCanBeStandaloneNullable(); 35 | } 36 | } 37 | 38 | /** 39 | * @internal 40 | * 41 | * @psalm-pure 42 | */ 43 | public static function fromReflectionType( 44 | ReflectionNamedType|ReflectionUnionType|ReflectionIntersectionType|null $type, 45 | ?ReflectionClass $currentClass 46 | ): ?self { 47 | if (null === $type) { 48 | return null; 49 | } 50 | 51 | if ($type instanceof ReflectionUnionType) { 52 | return new self( 53 | new UnionType(array_map( 54 | static fn( 55 | ReflectionIntersectionType|ReflectionNamedType $type 56 | ): IntersectionType|AtomicType => $type instanceof ReflectionNamedType 57 | ? AtomicType::fromReflectionNamedTypeAndClass($type, $currentClass) 58 | : self::fromIntersectionType($type, $currentClass), 59 | $type->getTypes() 60 | )), 61 | false 62 | ); 63 | } 64 | 65 | if ($type instanceof ReflectionIntersectionType) { 66 | return new self(self::fromIntersectionType($type, $currentClass), false); 67 | } 68 | 69 | $atomicType = AtomicType::fromReflectionNamedTypeAndClass($type, $currentClass); 70 | 71 | return new self( 72 | $atomicType, 73 | $atomicType->type !== 'mixed' && $atomicType->type !== 'null' && $type->allowsNull() 74 | ); 75 | } 76 | 77 | /** @psalm-pure */ 78 | private static function fromIntersectionType( 79 | ReflectionIntersectionType $intersectionType, 80 | ?ReflectionClass $currentClass 81 | ): IntersectionType { 82 | return new IntersectionType(array_map( 83 | static fn( 84 | ReflectionNamedType $type 85 | ): AtomicType => AtomicType::fromReflectionNamedTypeAndClass($type, $currentClass), 86 | $intersectionType->getTypes() 87 | )); 88 | } 89 | 90 | /** 91 | * @throws InvalidArgumentException 92 | * @psalm-pure 93 | */ 94 | public static function fromTypeString(string $type): self 95 | { 96 | [$nullable, $trimmedNullable] = self::trimNullable($type); 97 | 98 | if ( 99 | ! str_contains($trimmedNullable, CompositeType::INTERSECTION_SEPARATOR) 100 | && ! str_contains($trimmedNullable, CompositeType::UNION_SEPARATOR) 101 | ) { 102 | return new self(CompositeType::fromString($trimmedNullable), $nullable); 103 | } 104 | 105 | if ($nullable) { 106 | throw new InvalidArgumentException(sprintf( 107 | 'Type "%s" is a union type, and therefore cannot be also marked nullable with the "?" prefix', 108 | $type 109 | )); 110 | } 111 | 112 | return new self(CompositeType::fromString($trimmedNullable)); 113 | } 114 | 115 | /** 116 | * {@inheritDoc} 117 | * 118 | * Generates the type string, including FQCN "\\" prefix, so that 119 | * it can directly be used within any code snippet, regardless of 120 | * imports. 121 | * 122 | * @psalm-return non-empty-string 123 | */ 124 | public function generate(): string 125 | { 126 | return ($this->nullable ? self::NULL_MARKER : '') . $this->type->fullyQualifiedName(); 127 | } 128 | 129 | public function equals(TypeGenerator $otherType): bool 130 | { 131 | return $this->generate() === $otherType->generate(); 132 | } 133 | 134 | /** 135 | * @return non-empty-string the cleaned type string. Note that this value is not suitable for code generation, 136 | * since the returned value does not include any root namespace prefixes, when applicable, 137 | * and therefore the values cannot be used as FQCN in generated code. 138 | */ 139 | public function __toString(): string 140 | { 141 | return $this->type->toString(); 142 | } 143 | 144 | /** 145 | * @return bool[]|string[] ordered tuple, first key represents whether the type is nullable, second is the 146 | * trimmed string 147 | * @psalm-return array{bool, string} 148 | * @psalm-pure 149 | */ 150 | private static function trimNullable(string $type): array 151 | { 152 | if (str_starts_with($type, self::NULL_MARKER)) { 153 | return [true, substr($type, 1)]; 154 | } 155 | 156 | return [false, $type]; 157 | } 158 | } 159 | -------------------------------------------------------------------------------- /src/Generator/TypeGenerator/AtomicType.php: -------------------------------------------------------------------------------- 1 | 32 | */ 33 | private const BUILT_IN_TYPES_PRECEDENCE = [ 34 | 'bool' => 1, 35 | 'int' => 2, 36 | 'float' => 3, 37 | 'string' => 4, 38 | 'array' => 5, 39 | 'callable' => 6, 40 | 'iterable' => 7, 41 | 'object' => 8, 42 | 'static' => 9, 43 | 'mixed' => 10, 44 | 'void' => 11, 45 | 'false' => 12, 46 | 'true' => 13, 47 | 'null' => 14, 48 | 'never' => 15, 49 | ]; 50 | 51 | /** @psalm-var array */ 52 | private const NOT_NULLABLE_TYPES = [ 53 | 'null' => null, 54 | 'false' => null, 55 | 'true' => null, 56 | 'void' => null, 57 | 'mixed' => null, 58 | 'never' => null, 59 | ]; 60 | 61 | /** A regex pattern to match valid class/interface/trait names */ 62 | private const VALID_IDENTIFIER_MATCHER = '/^[a-zA-Z_\x80-\xff][a-zA-Z0-9_\x80-\xff]*' 63 | . '(\\\\[a-zA-Z_\x80-\xff][a-zA-Z0-9_\x80-\xff]*)*$/'; 64 | 65 | /** 66 | * @psalm-param non-empty-string $type 67 | * @psalm-param value-of|0 $sortIndex 68 | */ 69 | private function __construct( 70 | public string $type, 71 | public int $sortIndex 72 | ) { 73 | } 74 | 75 | /** 76 | * @psalm-pure 77 | * @throws InvalidArgumentException 78 | */ 79 | public static function fromString(string $type): self 80 | { 81 | $trimmedType = '\\' === ($type[0] ?? '') 82 | ? substr($type, 1) 83 | : $type; 84 | $lowerCaseType = strtolower($trimmedType); 85 | 86 | if (array_key_exists($lowerCaseType, self::BUILT_IN_TYPES_PRECEDENCE)) { 87 | if ($lowerCaseType !== strtolower($type)) { 88 | throw new InvalidArgumentException(sprintf( 89 | 'Provided type "%s" is a built-in type, and should not be prefixed with "\\"', 90 | $type 91 | )); 92 | } 93 | 94 | return new self($lowerCaseType, self::BUILT_IN_TYPES_PRECEDENCE[$lowerCaseType]); 95 | } 96 | 97 | if (1 !== preg_match(self::VALID_IDENTIFIER_MATCHER, $trimmedType)) { 98 | throw new InvalidArgumentException(sprintf( 99 | 'Provided type "%s" is not recognized as a valid expression: ' 100 | . 'it must match "%s" or be one of the built-in types (%s)', 101 | $type, 102 | self::VALID_IDENTIFIER_MATCHER, 103 | implode(', ', self::BUILT_IN_TYPES_PRECEDENCE) 104 | )); 105 | } 106 | 107 | assert('' !== $trimmedType); 108 | 109 | return new self($trimmedType, 0); 110 | } 111 | 112 | /** 113 | * @psalm-pure 114 | * @throws InvalidArgumentException 115 | */ 116 | public static function fromReflectionNamedTypeAndClass( 117 | ReflectionNamedType $type, 118 | ?ReflectionClass $currentClass 119 | ): self { 120 | $name = $type->getName(); 121 | $lowerCaseName = strtolower($name); 122 | 123 | if ('self' === $lowerCaseName && $currentClass) { 124 | return new self($currentClass->getName(), 0); 125 | } 126 | 127 | if ('parent' === $lowerCaseName && $currentClass && $parentClass = $currentClass->getParentClass()) { 128 | return new self($parentClass->getName(), 0); 129 | } 130 | 131 | return self::fromString($name); 132 | } 133 | 134 | /** @psalm-return non-empty-string */ 135 | public function fullyQualifiedName(): string 136 | { 137 | return array_key_exists($this->type, self::BUILT_IN_TYPES_PRECEDENCE) 138 | ? $this->type 139 | : '\\' . $this->type; 140 | } 141 | 142 | /** @return non-empty-string */ 143 | public function toString(): string 144 | { 145 | return $this->type; 146 | } 147 | 148 | /** @throws InvalidArgumentException */ 149 | public function assertCanUnionWith(self|IntersectionType $other): void 150 | { 151 | if ($other instanceof IntersectionType) { 152 | $other->assertCanUnionWith($this); 153 | 154 | return; 155 | } 156 | 157 | if ( 158 | 'mixed' === $this->type 159 | || 'void' === $this->type 160 | || 'never' === $this->type 161 | ) { 162 | throw new InvalidArgumentException(sprintf( 163 | 'Type "%s" cannot be composed in a union with any other types', 164 | $this->type 165 | )); 166 | } 167 | 168 | if ($other->type === $this->type) { 169 | throw new InvalidArgumentException(sprintf( 170 | 'Type "%s" cannot be composed in a union with the same type "%s"', 171 | $this->type, 172 | $other->type 173 | )); 174 | } 175 | 176 | if ( 177 | ('true' === $other->type && 'false' === $this->type) || 178 | ('false' === $other->type && 'true' === $this->type) 179 | ) { 180 | throw new InvalidArgumentException(sprintf( 181 | 'Type "%s" cannot be composed in a union with type "%s"', 182 | $this->type, 183 | $other->type 184 | )); 185 | } 186 | } 187 | 188 | /** @throws InvalidArgumentException */ 189 | public function assertCanIntersectWith(AtomicType $other): void 190 | { 191 | if (array_key_exists($this->type, self::BUILT_IN_TYPES_PRECEDENCE)) { 192 | throw new InvalidArgumentException(sprintf( 193 | 'Type "%s" cannot be composed in an intersection with any other types', 194 | $this->type 195 | )); 196 | } 197 | 198 | if ($other->type === $this->type) { 199 | throw new InvalidArgumentException(sprintf( 200 | 'Type "%s" cannot be composed in an intersection with the same type "%s"', 201 | $this->type, 202 | $other->type 203 | )); 204 | } 205 | } 206 | 207 | /** @throws InvalidArgumentException */ 208 | public function assertCanBeStandaloneNullable(): void 209 | { 210 | if (array_key_exists($this->type, self::NOT_NULLABLE_TYPES)) { 211 | throw new InvalidArgumentException(sprintf( 212 | 'Type "%s" cannot be nullable', 213 | $this->type 214 | )); 215 | } 216 | } 217 | } 218 | -------------------------------------------------------------------------------- /src/Generator/TypeGenerator/CompositeType.php: -------------------------------------------------------------------------------- 1 | $typesInUnion */ 44 | $typesInUnion = array_map( 45 | self::fromString(...), 46 | array_map( 47 | static fn (string $type): string => trim($type, '()'), 48 | explode(self::UNION_SEPARATOR, $type) 49 | ) 50 | ); 51 | 52 | return new UnionType($typesInUnion); 53 | } 54 | 55 | if (str_contains($type, self::INTERSECTION_SEPARATOR)) { 56 | /** @var non-empty-list $typesInIntersection */ 57 | $typesInIntersection = array_map(self::fromString(...), explode(self::INTERSECTION_SEPARATOR, $type)); 58 | 59 | return new IntersectionType($typesInIntersection); 60 | } 61 | 62 | return AtomicType::fromString($type); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/Generator/TypeGenerator/IntersectionType.php: -------------------------------------------------------------------------------- 1 | sorted, at least 2 values always present */ 25 | private readonly array $types; 26 | 27 | /** 28 | * @param non-empty-list $types at least 2 values needed 29 | * @throws InvalidArgumentException If the given types cannot intersect. 30 | */ 31 | public function __construct(array $types) 32 | { 33 | usort( 34 | $types, 35 | static fn(AtomicType $a, AtomicType $b): int => $a->type <=> $b->type 36 | ); 37 | 38 | foreach ($types as $index => $atomicType) { 39 | foreach (array_diff_key($types, array_flip([$index])) as $otherType) { 40 | $atomicType->assertCanIntersectWith($otherType); 41 | } 42 | } 43 | 44 | $this->types = $types; 45 | } 46 | 47 | /** @return non-empty-string */ 48 | public function toString(): string 49 | { 50 | return implode( 51 | '&', 52 | array_map(static fn(AtomicType $type): string => $type->toString(), $this->types) 53 | ); 54 | } 55 | 56 | /** @return non-empty-string */ 57 | public function fullyQualifiedName(): string 58 | { 59 | return implode( 60 | '&', 61 | array_map(static fn(AtomicType $type): string => $type->fullyQualifiedName(), $this->types) 62 | ); 63 | } 64 | 65 | /** @throws InvalidArgumentException When the union is not possible. */ 66 | public function assertCanUnionWith(AtomicType|self $other): void 67 | { 68 | if ($other instanceof AtomicType) { 69 | foreach ($this->types as $type) { 70 | $type->assertCanUnionWith($other); 71 | } 72 | 73 | return; 74 | } 75 | 76 | $thisString = $this->toString(); 77 | $otherString = $other->toString(); 78 | 79 | if (str_contains($thisString, $otherString) || str_contains($otherString, $thisString)) { 80 | throw new InvalidArgumentException(sprintf( 81 | 'Types "%s" and "%s" cannot be intersected, as they include each other', 82 | $thisString, 83 | $otherString 84 | )); 85 | } 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /src/Generator/TypeGenerator/UnionType.php: -------------------------------------------------------------------------------- 1 | $types sorted, at least 2 values always present */ 19 | private readonly array $types; 20 | 21 | /** @param non-empty-list $types at least 2 values needed */ 22 | public function __construct(array $types) 23 | { 24 | usort( 25 | $types, 26 | static fn(AtomicType|IntersectionType $a, AtomicType|IntersectionType $b): int => [ 27 | $a instanceof IntersectionType ? -1 : $a->sortIndex, 28 | $a->toString(), 29 | ] <=> [ 30 | $b instanceof IntersectionType ? -1 : $b->sortIndex, 31 | $b->toString(), 32 | ] 33 | ); 34 | 35 | foreach ($types as $index => $type) { 36 | foreach (array_diff_key($types, array_flip([$index])) as $otherType) { 37 | $type->assertCanUnionWith($otherType); 38 | } 39 | } 40 | 41 | $this->types = $types; 42 | } 43 | 44 | /** @return non-empty-string */ 45 | public function toString(): string 46 | { 47 | return implode( 48 | '|', 49 | array_map( 50 | static fn(AtomicType|IntersectionType $type): string => $type instanceof IntersectionType 51 | ? '(' . $type->toString() . ')' 52 | : $type->toString(), 53 | $this->types 54 | ) 55 | ); 56 | } 57 | 58 | /** @return non-empty-string */ 59 | public function fullyQualifiedName(): string 60 | { 61 | return implode( 62 | '|', 63 | array_map( 64 | static fn(AtomicType|IntersectionType $type): string => $type instanceof IntersectionType 65 | ? '(' . $type->fullyQualifiedName() . ')' 66 | : $type->fullyQualifiedName(), 67 | $this->types 68 | ) 69 | ); 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/Generic/Prototype/PrototypeClassFactory.php: -------------------------------------------------------------------------------- 1 | */ 25 | protected $prototypes = []; 26 | 27 | /** @var PrototypeGenericInterface|null */ 28 | protected $genericPrototype; 29 | 30 | /** 31 | * @param PrototypeInterface[] $prototypes 32 | */ 33 | public function __construct(array $prototypes = [], ?PrototypeGenericInterface $genericPrototype = null) 34 | { 35 | foreach ($prototypes as $prototype) { 36 | $this->addPrototype($prototype); 37 | } 38 | 39 | if ($genericPrototype) { 40 | $this->setGenericPrototype($genericPrototype); 41 | } 42 | } 43 | 44 | /** 45 | * @throws Exception\InvalidArgumentException 46 | */ 47 | public function addPrototype(PrototypeInterface $prototype): void 48 | { 49 | $prototypeName = $this->normalizeName($prototype->getName()); 50 | 51 | if (isset($this->prototypes[$prototypeName])) { 52 | throw new Exception\InvalidArgumentException('A prototype with this name already exists in this manager'); 53 | } 54 | 55 | $this->prototypes[$prototypeName] = $prototype; 56 | } 57 | 58 | /** 59 | * @throws Exception\InvalidArgumentException 60 | */ 61 | public function setGenericPrototype(PrototypeGenericInterface $prototype): void 62 | { 63 | if (isset($this->genericPrototype)) { 64 | throw new Exception\InvalidArgumentException('A default prototype is already set'); 65 | } 66 | 67 | $this->genericPrototype = $prototype; 68 | } 69 | 70 | /** 71 | * @param string $name 72 | * @return string 73 | */ 74 | protected function normalizeName($name) 75 | { 76 | return str_replace(['-', '_'], '', $name); 77 | } 78 | 79 | /** 80 | * @param string $name 81 | * @return bool 82 | */ 83 | public function hasPrototype($name) 84 | { 85 | $name = $this->normalizeName($name); 86 | return isset($this->prototypes[$name]); 87 | } 88 | 89 | /** 90 | * @param string $prototypeName 91 | * @return PrototypeInterface 92 | * @throws Exception\RuntimeException 93 | */ 94 | public function getClonedPrototype($prototypeName) 95 | { 96 | $prototypeName = $this->normalizeName($prototypeName); 97 | 98 | if (! $this->hasPrototype($prototypeName) && ! isset($this->genericPrototype)) { 99 | throw new Exception\RuntimeException('This tag name is not supported by this tag manager'); 100 | } 101 | 102 | if (! $this->hasPrototype($prototypeName)) { 103 | $newPrototype = clone $this->genericPrototype; 104 | $newPrototype->setName($prototypeName); 105 | 106 | return $newPrototype; 107 | } 108 | 109 | return clone $this->prototypes[$prototypeName]; 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /src/Generic/Prototype/PrototypeGenericInterface.php: -------------------------------------------------------------------------------- 1 | 21 | */ 22 | class ClassReflection extends ReflectionClass implements ReflectionInterface 23 | { 24 | /** @var DocBlockReflection|null */ 25 | protected $docBlock; 26 | 27 | /** 28 | * Return the classes DocBlock reflection object 29 | * 30 | * @return DocBlockReflection|false 31 | * @throws Exception\ExceptionInterface When missing DocBock or invalid reflection class. 32 | */ 33 | public function getDocBlock() 34 | { 35 | if (isset($this->docBlock)) { 36 | return $this->docBlock; 37 | } 38 | 39 | if ('' == $this->getDocComment()) { 40 | return false; 41 | } 42 | 43 | $this->docBlock = new DocBlockReflection($this); 44 | 45 | return $this->docBlock; 46 | } 47 | 48 | /** 49 | * {@inheritDoc} 50 | * 51 | * @param bool $includeDocComment 52 | * @return int|false 53 | */ 54 | #[ReturnTypeWillChange] 55 | public function getStartLine($includeDocComment = false) 56 | { 57 | if ($includeDocComment && $this->getDocComment() != '') { 58 | return $this->getDocBlock()->getStartLine(); 59 | } 60 | 61 | return parent::getStartLine(); 62 | } 63 | 64 | /** 65 | * Return the contents of the class 66 | * 67 | * @param bool $includeDocBlock 68 | * @return string 69 | */ 70 | public function getContents($includeDocBlock = true) 71 | { 72 | $fileName = $this->getFileName(); 73 | 74 | if (false === $fileName || ! file_exists($fileName)) { 75 | return ''; 76 | } 77 | 78 | $filelines = file($fileName); 79 | $startnum = $this->getStartLine($includeDocBlock); 80 | $endnum = $this->getEndLine() - $this->getStartLine(); 81 | 82 | // Ensure we get between the open and close braces 83 | $lines = array_slice($filelines, $startnum, $endnum); 84 | array_unshift($lines, $filelines[$startnum - 1]); 85 | 86 | return strstr(implode('', $lines), '{'); 87 | } 88 | 89 | /** 90 | * Get all reflection objects of implemented interfaces 91 | * 92 | * @return array 93 | */ 94 | #[ReturnTypeWillChange] 95 | public function getInterfaces() 96 | { 97 | return array_map( 98 | static fn (ReflectionClass $interface): ClassReflection => new ClassReflection($interface->getName()), 99 | parent::getInterfaces() 100 | ); 101 | } 102 | 103 | /** 104 | * Return method reflection by name 105 | * 106 | * @param string $name 107 | * @return MethodReflection 108 | */ 109 | #[ReturnTypeWillChange] 110 | public function getMethod($name) 111 | { 112 | return new MethodReflection($this->getName(), parent::getMethod($name)->getName()); 113 | } 114 | 115 | /** 116 | * {@inheritDoc} 117 | * 118 | * @param int $filter 119 | * @return list 120 | */ 121 | #[ReturnTypeWillChange] 122 | public function getMethods($filter = -1) 123 | { 124 | $name = $this->getName(); 125 | 126 | return array_map( 127 | static fn (ReflectionMethod $method): MethodReflection => new MethodReflection($name, $method->getName()), 128 | parent::getMethods($filter) 129 | ); 130 | } 131 | 132 | /** 133 | * {@inheritDoc} 134 | * 135 | * @return array 136 | */ 137 | #[ReturnTypeWillChange] 138 | public function getTraits() 139 | { 140 | return array_map( 141 | static fn (ReflectionClass $trait): ClassReflection => new ClassReflection($trait->getName()), 142 | parent::getTraits() 143 | ); 144 | } 145 | 146 | /** 147 | * {@inheritDoc} 148 | * 149 | * @return ClassReflection|false 150 | */ 151 | #[ReturnTypeWillChange] 152 | public function getParentClass() 153 | { 154 | $reflection = parent::getParentClass(); 155 | 156 | if (! $reflection) { 157 | return false; 158 | } 159 | 160 | return new ClassReflection($reflection->getName()); 161 | } 162 | 163 | /** 164 | * {@inheritDoc} 165 | * 166 | * @param string $name 167 | * @return PropertyReflection 168 | */ 169 | #[ReturnTypeWillChange] 170 | public function getProperty($name) 171 | { 172 | $phpReflection = parent::getProperty($name); 173 | $laminasReflection = new PropertyReflection($this->getName(), $phpReflection->getName()); 174 | unset($phpReflection); 175 | 176 | return $laminasReflection; 177 | } 178 | 179 | /** 180 | * {@inheritDoc} 181 | * 182 | * @param int $filter 183 | * @return list 184 | */ 185 | #[ReturnTypeWillChange] 186 | public function getProperties($filter = -1) 187 | { 188 | $name = $this->getName(); 189 | 190 | return array_map( 191 | static fn (ReflectionProperty $property): PropertyReflection 192 | => new PropertyReflection($name, $property->getName()), 193 | parent::getProperties($filter) 194 | ); 195 | } 196 | 197 | /** 198 | * @return string 199 | */ 200 | public function toString() 201 | { 202 | return parent::__toString(); 203 | } 204 | 205 | public function __toString(): string 206 | { 207 | return parent::__toString(); 208 | } 209 | } 210 | -------------------------------------------------------------------------------- /src/Reflection/DocBlock/Tag/AuthorTag.php: -------------------------------------------------------------------------------- 1 | ]*)\>)?(.*)$/u', $content, $match)) { 30 | return; 31 | } 32 | 33 | if ($match[1] !== '') { 34 | $this->authorName = rtrim($match[1]); 35 | } 36 | 37 | if (isset($match[3]) && $match[3] !== '') { 38 | $this->authorEmail = $match[3]; 39 | } 40 | } 41 | 42 | /** @return null|string */ 43 | public function getAuthorName() 44 | { 45 | return $this->authorName; 46 | } 47 | 48 | /** @return null|string */ 49 | public function getAuthorEmail() 50 | { 51 | return $this->authorEmail; 52 | } 53 | 54 | /** @return non-empty-string */ 55 | public function __toString(): string 56 | { 57 | return 'DocBlock Tag [ * @' . $this->getName() . ' ]' . "\n"; 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/Reflection/DocBlock/Tag/GenericTag.php: -------------------------------------------------------------------------------- 1 | */ 20 | protected $values = []; 21 | 22 | /** 23 | * @param string $contentSplitCharacter 24 | */ 25 | public function __construct(protected $contentSplitCharacter = ' ') 26 | { 27 | } 28 | 29 | /** @inheritDoc */ 30 | public function initialize($content) 31 | { 32 | $this->parse($content); 33 | } 34 | 35 | /** @return string|null */ 36 | public function getName() 37 | { 38 | return $this->name; 39 | } 40 | 41 | /** 42 | * @param string $name 43 | * @return void 44 | */ 45 | public function setName($name) 46 | { 47 | $this->name = $name; 48 | } 49 | 50 | /** @return string|null */ 51 | public function getContent() 52 | { 53 | return $this->content; 54 | } 55 | 56 | /** 57 | * @param int $position 58 | * @return string 59 | */ 60 | public function returnValue($position) 61 | { 62 | return $this->values[$position]; 63 | } 64 | 65 | /** @return non-empty-string */ 66 | public function __toString(): string 67 | { 68 | return 'DocBlock Tag [ * @' . $this->name . ' ]' . "\n"; 69 | } 70 | 71 | /** 72 | * @param string $docBlockLine 73 | * @return void 74 | */ 75 | protected function parse($docBlockLine) 76 | { 77 | $this->content = trim($docBlockLine); 78 | $this->values = explode($this->contentSplitCharacter, $docBlockLine); 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /src/Reflection/DocBlock/Tag/LicenseTag.php: -------------------------------------------------------------------------------- 1 | url = trim($match[1]); 35 | } 36 | 37 | if (isset($match[2]) && $match[2] !== '') { 38 | $this->licenseName = $match[2]; 39 | } 40 | } 41 | 42 | /** @return null|string */ 43 | public function getUrl() 44 | { 45 | return $this->url; 46 | } 47 | 48 | /** @return null|string */ 49 | public function getLicenseName() 50 | { 51 | return $this->licenseName; 52 | } 53 | 54 | /** @return non-empty-string */ 55 | public function __toString(): string 56 | { 57 | return 'DocBlock Tag [ * @' . $this->getName() . ' ]' . "\n"; 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/Reflection/DocBlock/Tag/MethodTag.php: -------------------------------------------------------------------------------- 1 | */ 14 | protected $types = []; 15 | 16 | /** @var string|null */ 17 | protected $methodName; 18 | 19 | /** @var string|null */ 20 | protected $description; 21 | 22 | /** @var bool */ 23 | protected $isStatic = false; 24 | 25 | /** @return 'method' */ 26 | public function getName() 27 | { 28 | return 'method'; 29 | } 30 | 31 | /** @inheritDoc */ 32 | public function initialize($content) 33 | { 34 | $match = []; 35 | 36 | if (! preg_match('#^(static[\s]+)?(.+[\s]+)?(.+\(\))[\s]*(.*)$#m', $content, $match)) { 37 | return; 38 | } 39 | 40 | if ($match[1] !== '') { 41 | $this->isStatic = true; 42 | } 43 | 44 | if ($match[2] !== '') { 45 | $this->types = explode('|', rtrim($match[2])); 46 | } 47 | 48 | $this->methodName = $match[3]; 49 | 50 | if ($match[4] !== '') { 51 | $this->description = $match[4]; 52 | } 53 | } 54 | 55 | /** 56 | * Get return value type 57 | * 58 | * @deprecated 2.0.4 use getTypes instead 59 | * 60 | * @return null|string 61 | */ 62 | public function getReturnType() 63 | { 64 | if (empty($this->types)) { 65 | return null; 66 | } 67 | 68 | return $this->types[0]; 69 | } 70 | 71 | /** @inheritDoc */ 72 | public function getTypes() 73 | { 74 | return $this->types; 75 | } 76 | 77 | /** @return string|null */ 78 | public function getMethodName() 79 | { 80 | return $this->methodName; 81 | } 82 | 83 | /** @return string|null */ 84 | public function getDescription() 85 | { 86 | return $this->description; 87 | } 88 | 89 | /** @return bool */ 90 | public function isStatic() 91 | { 92 | return $this->isStatic; 93 | } 94 | 95 | /** @return non-empty-string */ 96 | public function __toString(): string 97 | { 98 | return 'DocBlock Tag [ * @' . $this->getName() . ' ]' . "\n"; 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /src/Reflection/DocBlock/Tag/ParamTag.php: -------------------------------------------------------------------------------- 1 | */ 13 | protected $types = []; 14 | 15 | /** @var string|null */ 16 | protected $variableName; 17 | 18 | /** @var string|null */ 19 | protected $description; 20 | 21 | /** @return 'param' */ 22 | public function getName() 23 | { 24 | return 'param'; 25 | } 26 | 27 | /** @inheritDoc */ 28 | public function initialize($content) 29 | { 30 | $matches = []; 31 | 32 | if (! preg_match('#((?:[\w|\\\]+(?:\[\])*\|?)+)(?:\s+(\$\S+))?(?:\s+(.*))?#s', $content, $matches)) { 33 | return; 34 | } 35 | 36 | $this->types = explode('|', $matches[1]); 37 | 38 | if (isset($matches[2])) { 39 | $this->variableName = $matches[2]; 40 | } 41 | 42 | if (isset($matches[3])) { 43 | $this->description = trim(preg_replace('#\s+#', ' ', $matches[3])); 44 | } 45 | } 46 | 47 | /** 48 | * Get parameter variable type 49 | * 50 | * @deprecated 2.0.4 use getTypes instead 51 | * 52 | * @return string 53 | */ 54 | public function getType() 55 | { 56 | if (empty($this->types)) { 57 | return ''; 58 | } 59 | 60 | return $this->types[0]; 61 | } 62 | 63 | /** @inheritDoc */ 64 | public function getTypes() 65 | { 66 | return $this->types; 67 | } 68 | 69 | /** @return string|null */ 70 | public function getVariableName() 71 | { 72 | return $this->variableName; 73 | } 74 | 75 | /** @return string|null */ 76 | public function getDescription() 77 | { 78 | return $this->description; 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /src/Reflection/DocBlock/Tag/PhpDocTypedTagInterface.php: -------------------------------------------------------------------------------- 1 | 11 | */ 12 | public function getTypes(); 13 | } 14 | -------------------------------------------------------------------------------- /src/Reflection/DocBlock/Tag/PropertyTag.php: -------------------------------------------------------------------------------- 1 | */ 14 | protected $types = []; 15 | 16 | /** @var string|null */ 17 | protected $propertyName; 18 | 19 | /** @var string|null */ 20 | protected $description; 21 | 22 | /** 23 | * @return string 24 | */ 25 | public function getName() 26 | { 27 | return 'property'; 28 | } 29 | 30 | /** @inheritDoc */ 31 | public function initialize($content) 32 | { 33 | $match = []; 34 | if (! preg_match('#^(.+)?(\$[\S]+)[\s]*(.*)$#m', $content, $match)) { 35 | return; 36 | } 37 | 38 | if ($match[1] !== '') { 39 | $this->types = explode('|', rtrim($match[1])); 40 | } 41 | 42 | if ($match[2] !== '') { 43 | $this->propertyName = $match[2]; 44 | } 45 | 46 | if ($match[3] !== '') { 47 | $this->description = $match[3]; 48 | } 49 | } 50 | 51 | /** 52 | * @deprecated 2.0.4 use getTypes instead 53 | * 54 | * @return null|string 55 | */ 56 | public function getType() 57 | { 58 | if (empty($this->types)) { 59 | return null; 60 | } 61 | 62 | return $this->types[0]; 63 | } 64 | 65 | /** @inheritDoc */ 66 | public function getTypes() 67 | { 68 | return $this->types; 69 | } 70 | 71 | /** 72 | * @return null|string 73 | */ 74 | public function getPropertyName() 75 | { 76 | return $this->propertyName; 77 | } 78 | 79 | /** 80 | * @return null|string 81 | */ 82 | public function getDescription() 83 | { 84 | return $this->description; 85 | } 86 | 87 | /** 88 | * @psalm-return non-empty-string 89 | */ 90 | public function __toString(): string 91 | { 92 | return 'DocBlock Tag [ * @' . $this->getName() . ' ]' . "\n"; 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /src/Reflection/DocBlock/Tag/ReturnTag.php: -------------------------------------------------------------------------------- 1 | */ 13 | protected $types = []; 14 | 15 | /** @var string|null */ 16 | protected $description; 17 | 18 | /** 19 | * @return string 20 | */ 21 | public function getName() 22 | { 23 | return 'return'; 24 | } 25 | 26 | /** @inheritDoc */ 27 | public function initialize($content) 28 | { 29 | $matches = []; 30 | if (! preg_match('#((?:[\w|\\\]+(?:\[\])*\|?)+)(?:\s+(.*))?#s', $content, $matches)) { 31 | return; 32 | } 33 | 34 | $this->types = explode('|', $matches[1]); 35 | 36 | if (isset($matches[2])) { 37 | $this->description = trim(preg_replace('#\s+#', ' ', $matches[2])); 38 | } 39 | } 40 | 41 | /** 42 | * @deprecated 2.0.4 use getTypes instead 43 | * 44 | * @return string 45 | */ 46 | public function getType() 47 | { 48 | if (empty($this->types)) { 49 | return ''; 50 | } 51 | 52 | return $this->types[0]; 53 | } 54 | 55 | /** @inheritDoc */ 56 | public function getTypes() 57 | { 58 | return $this->types; 59 | } 60 | 61 | /** 62 | * @return string|null 63 | */ 64 | public function getDescription() 65 | { 66 | return $this->description; 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/Reflection/DocBlock/Tag/TagInterface.php: -------------------------------------------------------------------------------- 1 | 14 | */ 15 | protected $types = []; 16 | 17 | /** @var string|null */ 18 | protected $description; 19 | 20 | /** 21 | * @return string 22 | */ 23 | public function getName() 24 | { 25 | return 'throws'; 26 | } 27 | 28 | /** @inheritDoc */ 29 | public function initialize($content) 30 | { 31 | $matches = []; 32 | preg_match('#([\w|\\\]+)(?:\s+(.*))?#', $content, $matches); 33 | 34 | $this->types = explode('|', $matches[1]); 35 | 36 | if (isset($matches[2])) { 37 | $this->description = $matches[2]; 38 | } 39 | } 40 | 41 | /** 42 | * Get return variable type 43 | * 44 | * @deprecated 2.0.4 use getTypes instead 45 | * 46 | * @return string 47 | */ 48 | public function getType() 49 | { 50 | return implode('|', $this->getTypes()); 51 | } 52 | 53 | /** @inheritDoc */ 54 | public function getTypes() 55 | { 56 | return $this->types; 57 | } 58 | 59 | /** 60 | * @return string|null 61 | */ 62 | public function getDescription() 63 | { 64 | return $this->description; 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/Reflection/DocBlock/Tag/VarTag.php: -------------------------------------------------------------------------------- 1 | 18 | */ 19 | private $types = []; 20 | 21 | /** @var string|null */ 22 | private $variableName; 23 | 24 | /** @var string|null */ 25 | private $description; 26 | 27 | /** @inheritDoc */ 28 | public function getName(): string 29 | { 30 | return 'var'; 31 | } 32 | 33 | /** @inheritDoc */ 34 | public function initialize($content): void 35 | { 36 | $match = []; 37 | 38 | if ( 39 | ! preg_match( 40 | '#^([^\$]\S+)?\s*(\$[\S]+)?\s*(.*)$#m', 41 | $content, 42 | $match 43 | ) 44 | ) { 45 | return; 46 | } 47 | 48 | if ($match[1] !== '') { 49 | $this->types = explode('|', rtrim($match[1])); 50 | } 51 | 52 | if ($match[2] !== '') { 53 | $this->variableName = $match[2]; 54 | } 55 | 56 | if ($match[3] !== '') { 57 | $this->description = $match[3]; 58 | } 59 | } 60 | 61 | /** @inheritDoc */ 62 | public function getTypes(): array 63 | { 64 | return $this->types; 65 | } 66 | 67 | public function getVariableName(): ?string 68 | { 69 | return $this->variableName; 70 | } 71 | 72 | public function getDescription(): ?string 73 | { 74 | return $this->description; 75 | } 76 | 77 | /** 78 | * @psalm-return non-empty-string 79 | */ 80 | public function __toString(): string 81 | { 82 | return 'DocBlock Tag [ * @' . $this->getName() . ' ]' . PHP_EOL; 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /src/Reflection/DocBlock/TagManager.php: -------------------------------------------------------------------------------- 1 | addPrototype(new Tag\ParamTag()); 16 | $this->addPrototype(new Tag\ReturnTag()); 17 | $this->addPrototype(new Tag\MethodTag()); 18 | $this->addPrototype(new Tag\PropertyTag()); 19 | $this->addPrototype(new Tag\AuthorTag()); 20 | $this->addPrototype(new Tag\LicenseTag()); 21 | $this->addPrototype(new Tag\ThrowsTag()); 22 | $this->addPrototype(new Tag\VarTag()); 23 | $this->setGenericPrototype(new Tag\GenericTag()); 24 | } 25 | 26 | /** 27 | * @param string $tagName 28 | * @param string $content 29 | * @return TagInterface 30 | */ 31 | public function createTag($tagName, $content = null) 32 | { 33 | /** @var TagInterface $newTag */ 34 | $newTag = $this->getClonedPrototype($tagName); 35 | 36 | if ($content) { 37 | $newTag->initialize($content); 38 | } 39 | 40 | return $newTag; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/Reflection/DocBlockReflection.php: -------------------------------------------------------------------------------- 1 | initializeDefaultTags(); 71 | } 72 | $this->tagManager = $tagManager; 73 | 74 | if ($commentOrReflector instanceof Reflector) { 75 | $this->reflector = $commentOrReflector; 76 | if (! method_exists($commentOrReflector, 'getDocComment')) { 77 | throw new Exception\InvalidArgumentException('Reflector must contain method "getDocComment"'); 78 | } 79 | 80 | $this->docComment = $commentOrReflector->getDocComment(); 81 | 82 | // determine line numbers 83 | $lineCount = substr_count($this->docComment, "\n"); 84 | $this->startLine = $this->reflector->getStartLine() - $lineCount - 1; 85 | $this->endLine = $this->reflector->getStartLine() - 1; 86 | } elseif (is_string($commentOrReflector)) { 87 | $this->docComment = $commentOrReflector; 88 | } else { 89 | throw new Exception\InvalidArgumentException(sprintf( 90 | '%s must have a (string) DocComment or a Reflector in the constructor', 91 | static::class 92 | )); 93 | } 94 | 95 | if ($this->docComment == '') { 96 | throw new Exception\InvalidArgumentException('DocComment cannot be empty'); 97 | } 98 | 99 | $this->reflect(); 100 | } 101 | 102 | /** 103 | * Retrieve contents of DocBlock 104 | * 105 | * @return string 106 | */ 107 | public function getContents() 108 | { 109 | $this->reflect(); 110 | 111 | return $this->cleanDocComment; 112 | } 113 | 114 | /** 115 | * Get start line (position) of DocBlock 116 | * 117 | * @return int 118 | */ 119 | public function getStartLine() 120 | { 121 | $this->reflect(); 122 | 123 | return $this->startLine; 124 | } 125 | 126 | /** 127 | * Get last line (position) of DocBlock 128 | * 129 | * @return int 130 | */ 131 | public function getEndLine() 132 | { 133 | $this->reflect(); 134 | 135 | return $this->endLine; 136 | } 137 | 138 | /** 139 | * Get DocBlock short description 140 | * 141 | * @return string 142 | */ 143 | public function getShortDescription() 144 | { 145 | $this->reflect(); 146 | 147 | return $this->shortDescription; 148 | } 149 | 150 | /** 151 | * Get DocBlock long description 152 | * 153 | * @return string 154 | */ 155 | public function getLongDescription() 156 | { 157 | $this->reflect(); 158 | 159 | return $this->longDescription; 160 | } 161 | 162 | /** 163 | * Does the DocBlock contain the given annotation tag? 164 | * 165 | * @param string $name 166 | * @return bool 167 | */ 168 | public function hasTag($name) 169 | { 170 | $this->reflect(); 171 | foreach ($this->tags as $tag) { 172 | if ($tag->getName() == $name) { 173 | return true; 174 | } 175 | } 176 | 177 | return false; 178 | } 179 | 180 | /** 181 | * Retrieve the given DocBlock tag 182 | * 183 | * @param string $name 184 | * @return DocBlockTagInterface|false 185 | */ 186 | public function getTag($name) 187 | { 188 | $this->reflect(); 189 | foreach ($this->tags as $tag) { 190 | if ($tag->getName() == $name) { 191 | return $tag; 192 | } 193 | } 194 | 195 | return false; 196 | } 197 | 198 | /** 199 | * Get all DocBlock annotation tags 200 | * 201 | * @param string $filter 202 | * @return DocBlockTagInterface[] 203 | */ 204 | public function getTags($filter = null) 205 | { 206 | $this->reflect(); 207 | if ($filter === null || ! is_string($filter)) { 208 | return $this->tags; 209 | } 210 | 211 | $returnTags = []; 212 | foreach ($this->tags as $tag) { 213 | if ($tag->getName() == $filter) { 214 | $returnTags[] = $tag; 215 | } 216 | } 217 | 218 | return $returnTags; 219 | } 220 | 221 | /** 222 | * Parse the DocBlock 223 | * 224 | * @return void 225 | */ 226 | protected function reflect() 227 | { 228 | if ($this->isReflected) { 229 | return; 230 | } 231 | 232 | $docComment = preg_replace('#[ ]{0,1}\*/$#', '', $this->docComment); 233 | 234 | // create a clean docComment 235 | $this->cleanDocComment = preg_replace("#[ \t]*(?:/\*\*|\*/|\*)[ ]{0,1}(.*)?#", '$1', $docComment); 236 | 237 | // @todo should be changed to remove first and last empty line 238 | $this->cleanDocComment = ltrim($this->cleanDocComment, "\r\n"); 239 | 240 | $scanner = new DocBlockScanner($docComment); 241 | $this->shortDescription = ltrim($scanner->getShortDescription()); 242 | $this->longDescription = ltrim($scanner->getLongDescription()); 243 | 244 | foreach ($scanner->getTags() as $tag) { 245 | $this->tags[] = $this->tagManager->createTag(ltrim($tag['name'], '@'), ltrim($tag['value'])); 246 | } 247 | 248 | $this->isReflected = true; 249 | } 250 | 251 | /** 252 | * @return string 253 | */ 254 | public function toString() 255 | { 256 | $str = 'DocBlock [ /* DocBlock */ ] {' . "\n\n"; 257 | $str .= ' - Tags [' . count($this->tags) . '] {' . "\n"; 258 | 259 | foreach ($this->tags as $tag) { 260 | $str .= ' ' . $tag; 261 | } 262 | 263 | $str .= ' }' . "\n"; 264 | $str .= '}' . "\n"; 265 | 266 | return $str; 267 | } 268 | 269 | /** 270 | * Serialize to string 271 | * 272 | * Required by the Reflector interface 273 | */ 274 | public function __toString(): string 275 | { 276 | return $this->toString(); 277 | } 278 | } 279 | -------------------------------------------------------------------------------- /src/Reflection/Exception/BadMethodCallException.php: -------------------------------------------------------------------------------- 1 | getDocComment())) { 46 | throw new Exception\InvalidArgumentException(sprintf( 47 | '%s does not have a DocBlock', 48 | $this->getName() 49 | )); 50 | } 51 | 52 | return new DocBlockReflection($comment); 53 | } 54 | 55 | /** 56 | * Get start line (position) of function 57 | * 58 | * @param bool $includeDocComment 59 | * @return int 60 | */ 61 | #[ReturnTypeWillChange] 62 | public function getStartLine($includeDocComment = false) 63 | { 64 | if ($includeDocComment) { 65 | if ($this->getDocComment() != '') { 66 | return $this->getDocBlock()->getStartLine(); 67 | } 68 | } 69 | 70 | return parent::getStartLine(); 71 | } 72 | 73 | /** 74 | * Get contents of function 75 | * 76 | * @param bool $includeDocBlock 77 | * @return string 78 | */ 79 | public function getContents($includeDocBlock = true) 80 | { 81 | $fileName = $this->getFileName(); 82 | if (false === $fileName) { 83 | return ''; 84 | } 85 | 86 | $startLine = $this->getStartLine(); 87 | $endLine = $this->getEndLine(); 88 | 89 | // eval'd protect 90 | if (preg_match('#\((\d+)\) : eval\(\)\'d code$#', $fileName, $matches)) { 91 | $fileName = preg_replace('#\(\d+\) : eval\(\)\'d code$#', '', $fileName); 92 | $startLine = $endLine = $matches[1]; 93 | } 94 | 95 | $lines = array_slice( 96 | file($fileName, FILE_IGNORE_NEW_LINES), 97 | $startLine - 1, 98 | $endLine - ($startLine - 1), 99 | true 100 | ); 101 | 102 | $functionLine = implode("\n", $lines); 103 | 104 | $content = ''; 105 | if ($this->isClosure()) { 106 | preg_match('#function\s*\([^\)]*\)\s*(use\s*\([^\)]+\))?\s*\{(.*\;)?\s*\}#s', $functionLine, $matches); 107 | if (isset($matches[0])) { 108 | $content = $matches[0]; 109 | } 110 | } else { 111 | $name = substr($this->getName(), strrpos($this->getName(), '\\') + 1); 112 | preg_match( 113 | '#function\s+' . preg_quote($name) . '\s*\([^\)]*\)\s*{([^{}]+({[^}]+})*[^}]+)?}#', 114 | $functionLine, 115 | $matches 116 | ); 117 | if (isset($matches[0])) { 118 | $content = $matches[0]; 119 | } 120 | } 121 | 122 | $docComment = $this->getDocComment(); 123 | 124 | return $includeDocBlock && $docComment ? $docComment . "\n" . $content : $content; 125 | } 126 | 127 | /** 128 | * Get method prototype 129 | * 130 | * @deprecated this method is unreliable, and should not be used: it will be removed in the next major release. 131 | * It may crash on parameters with union types, and will return relative types, instead of 132 | * FQN references 133 | * 134 | * @param string $format 135 | * @return array|string 136 | */ 137 | public function getPrototype($format = self::PROTOTYPE_AS_ARRAY) 138 | { 139 | $docBlock = $this->getDocBlock(); 140 | $return = $docBlock->getTag('return'); 141 | $returnTypes = $return->getTypes(); 142 | $returnType = count($returnTypes) > 1 ? implode('|', $returnTypes) : $returnTypes[0]; 143 | 144 | $prototype = [ 145 | 'namespace' => $this->getNamespaceName(), 146 | 'name' => substr($this->getName(), strlen($this->getNamespaceName()) + 1), 147 | 'return' => $returnType, 148 | 'arguments' => [], 149 | ]; 150 | 151 | $parameters = $this->getParameters(); 152 | foreach ($parameters as $parameter) { 153 | $prototype['arguments'][$parameter->getName()] = [ 154 | 'type' => $parameter->detectType(), 155 | 'required' => ! $parameter->isOptional(), 156 | 'by_ref' => $parameter->isPassedByReference(), 157 | 'default' => $parameter->isDefaultValueAvailable() ? $parameter->getDefaultValue() : null, 158 | ]; 159 | } 160 | 161 | if ($format == self::PROTOTYPE_AS_STRING) { 162 | $line = $prototype['return'] . ' ' . $prototype['name'] . '('; 163 | $args = []; 164 | foreach ($prototype['arguments'] as $name => $argument) { 165 | $argsLine = ($argument['type'] 166 | ? $argument['type'] . ' ' 167 | : '') . ($argument['by_ref'] ? '&' : '') . '$' . $name; 168 | if (! $argument['required']) { 169 | $argsLine .= ' = ' . var_export($argument['default'], true); 170 | } 171 | $args[] = $argsLine; 172 | } 173 | $line .= implode(', ', $args); 174 | $line .= ')'; 175 | 176 | return $line; 177 | } 178 | 179 | return $prototype; 180 | } 181 | 182 | /** 183 | * Get function parameters 184 | * 185 | * @return list 186 | */ 187 | #[ReturnTypeWillChange] 188 | public function getParameters() 189 | { 190 | $name = $this->getName(); 191 | 192 | return array_map( 193 | static fn (ReflectionParameter $parameter): ParameterReflection 194 | => new ParameterReflection($name, $parameter->getName()), 195 | parent::getParameters() 196 | ); 197 | } 198 | 199 | /** 200 | * Get return type tag 201 | * 202 | * @deprecated this method is unreliable, and will be dropped in the next major release. 203 | * If you are attempting to inspect the return type of an expression, please 204 | * use more reliable tools, such as `vimeo/psalm` or `phpstan/phpstan` instead. 205 | * 206 | * @throws Exception\InvalidArgumentException 207 | * @return DocBlockReflection 208 | */ 209 | public function getReturn() 210 | { 211 | $docBlock = $this->getDocBlock(); 212 | if (! $docBlock->hasTag('return')) { 213 | throw new Exception\InvalidArgumentException( 214 | 'Function does not specify an @return annotation tag; cannot determine return type' 215 | ); 216 | } 217 | 218 | $tag = $docBlock->getTag('return'); 219 | 220 | return new DocBlockReflection('@return ' . $tag->getDescription()); 221 | } 222 | 223 | /** 224 | * Get method body 225 | * 226 | * @return string|false 227 | */ 228 | public function getBody() 229 | { 230 | $fileName = $this->getFileName(); 231 | if (false === $fileName) { 232 | throw new Exception\InvalidArgumentException( 233 | 'Cannot determine internals functions body' 234 | ); 235 | } 236 | 237 | $startLine = $this->getStartLine(); 238 | $endLine = $this->getEndLine(); 239 | 240 | // eval'd protect 241 | if (preg_match('#\((\d+)\) : eval\(\)\'d code$#', $fileName, $matches)) { 242 | $fileName = preg_replace('#\(\d+\) : eval\(\)\'d code$#', '', $fileName); 243 | $startLine = $endLine = $matches[1]; 244 | } 245 | 246 | $lines = array_slice( 247 | file($fileName, FILE_IGNORE_NEW_LINES), 248 | $startLine - 1, 249 | $endLine - ($startLine - 1), 250 | true 251 | ); 252 | 253 | $functionLine = implode("\n", $lines); 254 | 255 | $body = false; 256 | if ($this->isClosure()) { 257 | preg_match('#function\s*\([^\)]*\)\s*(use\s*\([^\)]+\))?\s*\{(.*\;)\s*\}#s', $functionLine, $matches); 258 | if (isset($matches[2])) { 259 | $body = $matches[2]; 260 | } 261 | } else { 262 | $name = substr($this->getName(), strrpos($this->getName(), '\\') + 1); 263 | preg_match('#function\s+' . $name . '\s*\([^\)]*\)\s*{([^{}]+({[^}]+})*[^}]+)}#', $functionLine, $matches); 264 | if (isset($matches[1])) { 265 | $body = $matches[1]; 266 | } 267 | } 268 | 269 | return $body; 270 | } 271 | 272 | /** 273 | * @return string 274 | */ 275 | public function toString() 276 | { 277 | return $this->__toString(); 278 | } 279 | 280 | /** 281 | * Required due to bug in php 282 | * 283 | * @psalm-external-mutation-free 284 | */ 285 | public function __toString(): string 286 | { 287 | return parent::__toString(); 288 | } 289 | } 290 | -------------------------------------------------------------------------------- /src/Reflection/ParameterReflection.php: -------------------------------------------------------------------------------- 1 | getName()); 36 | } 37 | 38 | /** 39 | * Get class reflection object 40 | * 41 | * @return null|ClassReflection 42 | */ 43 | #[ReturnTypeWillChange] 44 | public function getClass() 45 | { 46 | $type = parent::getType(); 47 | 48 | if (! $type instanceof ReflectionNamedType || $type->isBuiltin()) { 49 | return null; 50 | } 51 | 52 | return new ClassReflection($type->getName()); 53 | } 54 | 55 | /** 56 | * Get declaring function reflection object 57 | * 58 | * @return FunctionReflection|MethodReflection 59 | */ 60 | #[ReturnTypeWillChange] 61 | public function getDeclaringFunction() 62 | { 63 | $function = parent::getDeclaringFunction(); 64 | 65 | if ($function instanceof ReflectionMethod) { 66 | return new MethodReflection($function->getDeclaringClass()->getName(), $function->getName()); 67 | } 68 | 69 | return new FunctionReflection($function->getName()); 70 | } 71 | 72 | /** 73 | * Get parameter type 74 | * 75 | * @deprecated this method is unreliable, and should not be used: it will be removed in the next major release. 76 | * It may crash on parameters with union types, and will return relative types, instead of 77 | * FQN references 78 | * 79 | * @return string|null 80 | */ 81 | public function detectType() 82 | { 83 | if ( 84 | null !== ($type = $this->getType()) 85 | && $type->isBuiltin() 86 | ) { 87 | return $type->getName(); 88 | } 89 | 90 | if (null !== $type && $type->getName() === 'self') { 91 | $declaringClass = $this->getDeclaringClass(); 92 | 93 | assert($declaringClass !== null, 'A parameter called `self` can only exist on a class'); 94 | 95 | return $declaringClass->getName(); 96 | } 97 | 98 | if (($class = $this->getClass()) instanceof ReflectionClass) { 99 | return $class->getName(); 100 | } 101 | 102 | $docBlock = $this->getDeclaringFunction()->getDocBlock(); 103 | 104 | if (! $docBlock instanceof DocBlockReflection) { 105 | return null; 106 | } 107 | 108 | /** @var ParamTag[] $params */ 109 | $params = $docBlock->getTags('param'); 110 | $paramTag = $params[$this->getPosition()] ?? null; 111 | $variableName = '$' . $this->getName(); 112 | 113 | if ($paramTag && ('' === $paramTag->getVariableName() || $variableName === $paramTag->getVariableName())) { 114 | return $paramTag->getTypes()[0] ?? ''; 115 | } 116 | 117 | foreach ($params as $param) { 118 | if ($param->getVariableName() === $variableName) { 119 | return $param->getTypes()[0] ?? ''; 120 | } 121 | } 122 | 123 | return null; 124 | } 125 | 126 | /** 127 | * @return string 128 | */ 129 | public function toString() 130 | { 131 | return parent::__toString(); 132 | } 133 | 134 | public function isPublicPromoted(): bool 135 | { 136 | $property = $this->promotedProperty(); 137 | 138 | if ($property === null) { 139 | return false; 140 | } 141 | 142 | return (bool) ($property->getModifiers() & ReflectionProperty::IS_PUBLIC); 143 | } 144 | 145 | public function isProtectedPromoted(): bool 146 | { 147 | $property = $this->promotedProperty(); 148 | 149 | if ($property === null) { 150 | return false; 151 | } 152 | 153 | return (bool) ($property->getModifiers() & ReflectionProperty::IS_PROTECTED); 154 | } 155 | 156 | public function isPrivatePromoted(): bool 157 | { 158 | $property = $this->promotedProperty(); 159 | 160 | if ($property === null) { 161 | return false; 162 | } 163 | 164 | return (bool) ($property->getModifiers() & ReflectionProperty::IS_PRIVATE); 165 | } 166 | 167 | private function promotedProperty(): ?ReflectionProperty 168 | { 169 | if (! $this->isPromoted()) { 170 | return null; 171 | } 172 | 173 | $declaringClass = $this->getDeclaringClass(); 174 | 175 | assert($declaringClass !== null, 'Promoted properties are always part of a class'); 176 | 177 | return $declaringClass->getProperty($this->getName()); 178 | } 179 | } 180 | -------------------------------------------------------------------------------- /src/Reflection/PropertyReflection.php: -------------------------------------------------------------------------------- 1 | getName()); 23 | unset($phpReflection); 24 | 25 | return $laminasReflection; 26 | } 27 | 28 | /** 29 | * Get DocBlock comment 30 | * 31 | * @return string|false False if no DocBlock defined 32 | */ 33 | #[ReturnTypeWillChange] 34 | public function getDocComment() 35 | { 36 | return parent::getDocComment(); 37 | } 38 | 39 | /** 40 | * @return false|DocBlockReflection 41 | */ 42 | public function getDocBlock() 43 | { 44 | if (! ($docComment = $this->getDocComment())) { 45 | return false; 46 | } 47 | 48 | return new DocBlockReflection($docComment); 49 | } 50 | 51 | /** 52 | * @return string 53 | */ 54 | public function toString() 55 | { 56 | return $this->__toString(); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/Reflection/ReflectionInterface.php: -------------------------------------------------------------------------------- 1 | scan(); 47 | 48 | return $this->shortDescription; 49 | } 50 | 51 | /** 52 | * @return string 53 | */ 54 | public function getLongDescription() 55 | { 56 | $this->scan(); 57 | 58 | return $this->longDescription; 59 | } 60 | 61 | /** 62 | * @return array 63 | */ 64 | public function getTags() 65 | { 66 | $this->scan(); 67 | 68 | return $this->tags; 69 | } 70 | 71 | /** 72 | * @return void 73 | */ 74 | protected function scan() 75 | { 76 | if ($this->isScanned) { 77 | return; 78 | } 79 | 80 | $mode = 1; 81 | 82 | $tokens = $this->tokenize(); 83 | $tagIndex = null; 84 | reset($tokens); 85 | 86 | SCANNER_TOP: 87 | $token = current($tokens); 88 | 89 | switch ($token[0]) { 90 | case 'DOCBLOCK_NEWLINE': 91 | if ($this->shortDescription != '' && $tagIndex === null) { 92 | $mode = 2; 93 | } else { 94 | $this->longDescription .= $token[1]; 95 | } 96 | goto SCANNER_CONTINUE; 97 | //goto no break needed 98 | 99 | case 'DOCBLOCK_WHITESPACE': 100 | case 'DOCBLOCK_TEXT': 101 | if ($tagIndex !== null) { 102 | $this->tags[$tagIndex]['value'] .= $this->tags[$tagIndex]['value'] == '' 103 | ? $token[1] 104 | : ' ' . $token[1]; 105 | goto SCANNER_CONTINUE; 106 | } elseif ($mode <= 2) { 107 | if ($mode == 1) { 108 | $this->shortDescription .= $token[1]; 109 | } else { 110 | $this->longDescription .= $token[1]; 111 | } 112 | goto SCANNER_CONTINUE; 113 | } 114 | //gotos no break needed 115 | case 'DOCBLOCK_TAG': 116 | array_push($this->tags, [ 117 | 'name' => $token[1], 118 | 'value' => '', 119 | ]); 120 | $tagIndex = array_key_last($this->tags); 121 | $mode = 3; 122 | goto SCANNER_CONTINUE; 123 | //goto no break needed 124 | 125 | case 'DOCBLOCK_COMMENTEND': 126 | goto SCANNER_END; 127 | } 128 | 129 | SCANNER_CONTINUE: 130 | if (next($tokens) === false) { 131 | goto SCANNER_END; 132 | } 133 | goto SCANNER_TOP; 134 | 135 | SCANNER_END: 136 | 137 | $this->shortDescription = trim($this->shortDescription); 138 | $this->longDescription = trim($this->longDescription); 139 | $this->isScanned = true; 140 | } 141 | 142 | /** 143 | * @phpcs:disable Generic.Formatting.MultipleStatementAlignment.NotSame 144 | * @return array 145 | */ 146 | protected function tokenize() 147 | { 148 | static $CONTEXT_INSIDE_DOCBLOCK = 0x01; 149 | static $CONTEXT_INSIDE_ASTERISK = 0x02; 150 | 151 | $context = 0x00; 152 | $stream = $this->docComment; 153 | $streamIndex = null; 154 | $tokens = []; 155 | $tokenIndex = null; 156 | $currentChar = null; 157 | $currentWord = null; 158 | $currentLine = null; 159 | 160 | $MACRO_STREAM_ADVANCE_CHAR = function ($positionsForward = 1) use ( 161 | &$stream, 162 | &$streamIndex, 163 | &$currentChar, 164 | &$currentWord, 165 | &$currentLine 166 | ) { 167 | $positionsForward = $positionsForward > 0 ? $positionsForward : 1; 168 | $streamIndex = $streamIndex === null ? 0 : $streamIndex + $positionsForward; 169 | if (! isset($stream[$streamIndex])) { 170 | $currentChar = false; 171 | 172 | return false; 173 | } 174 | $currentChar = $stream[$streamIndex]; 175 | $matches = []; 176 | $currentLine = preg_match('#(.*?)\r?\n#', $stream, $matches, 0, $streamIndex) === 1 177 | ? $matches[1] 178 | : substr($stream, $streamIndex); 179 | if ($currentChar === ' ') { 180 | $currentWord = preg_match('#( +)#', $currentLine, $matches) === 1 ? $matches[1] : $currentLine; 181 | } else { 182 | $currentWord = ($matches = strpos($currentLine, ' ')) !== false 183 | ? substr($currentLine, 0, $matches) 184 | : $currentLine; 185 | } 186 | 187 | return $currentChar; 188 | }; 189 | $MACRO_STREAM_ADVANCE_WORD = function () use (&$currentWord, &$MACRO_STREAM_ADVANCE_CHAR) { 190 | return $MACRO_STREAM_ADVANCE_CHAR(strlen($currentWord)); 191 | }; 192 | $MACRO_STREAM_ADVANCE_LINE = function () use (&$currentLine, &$MACRO_STREAM_ADVANCE_CHAR) { 193 | return $MACRO_STREAM_ADVANCE_CHAR(strlen($currentLine)); 194 | }; 195 | $MACRO_TOKEN_ADVANCE = function () use (&$tokenIndex, &$tokens) { 196 | $tokenIndex = $tokenIndex === null ? 0 : $tokenIndex + 1; 197 | $tokens[$tokenIndex] = ['DOCBLOCK_UNKNOWN', '']; 198 | }; 199 | $MACRO_TOKEN_SET_TYPE = function ($type) use (&$tokenIndex, &$tokens) { 200 | $tokens[$tokenIndex][0] = $type; 201 | }; 202 | $MACRO_TOKEN_APPEND_CHAR = function () use (&$currentChar, &$tokens, &$tokenIndex) { 203 | $tokens[$tokenIndex][1] .= $currentChar; 204 | }; 205 | $MACRO_TOKEN_APPEND_WORD = function () use (&$currentWord, &$tokens, &$tokenIndex) { 206 | $tokens[$tokenIndex][1] .= $currentWord; 207 | }; 208 | $MACRO_TOKEN_APPEND_WORD_PARTIAL = function ($length) use (&$currentWord, &$tokens, &$tokenIndex) { 209 | $tokens[$tokenIndex][1] .= substr($currentWord, 0, $length); 210 | }; 211 | $MACRO_TOKEN_APPEND_LINE = function () use (&$currentLine, &$tokens, &$tokenIndex) { 212 | $tokens[$tokenIndex][1] .= $currentLine; 213 | }; 214 | 215 | $MACRO_STREAM_ADVANCE_CHAR(); 216 | $MACRO_TOKEN_ADVANCE(); 217 | 218 | TOKENIZER_TOP: 219 | 220 | if ($context === 0x00 && $currentChar === '/' && $currentWord === '/**') { 221 | $MACRO_TOKEN_SET_TYPE('DOCBLOCK_COMMENTSTART'); 222 | $MACRO_TOKEN_APPEND_WORD(); 223 | $MACRO_TOKEN_ADVANCE(); 224 | $context |= $CONTEXT_INSIDE_DOCBLOCK; 225 | $context |= $CONTEXT_INSIDE_ASTERISK; 226 | if ($MACRO_STREAM_ADVANCE_WORD() === false) { 227 | goto TOKENIZER_END; 228 | } 229 | goto TOKENIZER_TOP; 230 | } 231 | 232 | if ($context & $CONTEXT_INSIDE_DOCBLOCK && $currentWord === '*/') { 233 | $MACRO_TOKEN_SET_TYPE('DOCBLOCK_COMMENTEND'); 234 | $MACRO_TOKEN_APPEND_WORD(); 235 | $MACRO_TOKEN_ADVANCE(); 236 | $context &= ~$CONTEXT_INSIDE_DOCBLOCK; 237 | if ($MACRO_STREAM_ADVANCE_WORD() === false) { 238 | goto TOKENIZER_END; 239 | } 240 | goto TOKENIZER_TOP; 241 | } 242 | 243 | if ($currentChar === ' ' || $currentChar === "\t") { 244 | $MACRO_TOKEN_SET_TYPE( 245 | $context & $CONTEXT_INSIDE_ASTERISK 246 | ? 'DOCBLOCK_WHITESPACE' 247 | : 'DOCBLOCK_WHITESPACE_INDENT' 248 | ); 249 | $MACRO_TOKEN_APPEND_WORD(); 250 | $MACRO_TOKEN_ADVANCE(); 251 | if ($MACRO_STREAM_ADVANCE_WORD() === false) { 252 | goto TOKENIZER_END; 253 | } 254 | goto TOKENIZER_TOP; 255 | } 256 | 257 | if ($currentChar === '*') { 258 | if (($context & $CONTEXT_INSIDE_DOCBLOCK) && ($context & $CONTEXT_INSIDE_ASTERISK)) { 259 | $MACRO_TOKEN_SET_TYPE('DOCBLOCK_TEXT'); 260 | } else { 261 | $MACRO_TOKEN_SET_TYPE('DOCBLOCK_ASTERISK'); 262 | $context |= $CONTEXT_INSIDE_ASTERISK; 263 | } 264 | $MACRO_TOKEN_APPEND_CHAR(); 265 | $MACRO_TOKEN_ADVANCE(); 266 | if ($MACRO_STREAM_ADVANCE_CHAR() === false) { 267 | goto TOKENIZER_END; 268 | } 269 | goto TOKENIZER_TOP; 270 | } 271 | 272 | if ($currentChar === '@') { 273 | $MACRO_TOKEN_SET_TYPE('DOCBLOCK_TAG'); 274 | $MACRO_TOKEN_APPEND_WORD(); 275 | $MACRO_TOKEN_ADVANCE(); 276 | if ($MACRO_STREAM_ADVANCE_WORD() === false) { 277 | goto TOKENIZER_END; 278 | } 279 | goto TOKENIZER_TOP; 280 | } 281 | 282 | if ($currentChar === "\n") { 283 | $MACRO_TOKEN_SET_TYPE('DOCBLOCK_NEWLINE'); 284 | $MACRO_TOKEN_APPEND_CHAR(); 285 | $MACRO_TOKEN_ADVANCE(); 286 | $context &= ~$CONTEXT_INSIDE_ASTERISK; 287 | if ($MACRO_STREAM_ADVANCE_CHAR() === false) { 288 | goto TOKENIZER_END; 289 | } 290 | goto TOKENIZER_TOP; 291 | } 292 | 293 | $MACRO_TOKEN_SET_TYPE('DOCBLOCK_TEXT'); 294 | $MACRO_TOKEN_APPEND_LINE(); 295 | $MACRO_TOKEN_ADVANCE(); 296 | if ($MACRO_STREAM_ADVANCE_LINE() === false) { 297 | goto TOKENIZER_END; 298 | } 299 | goto TOKENIZER_TOP; 300 | 301 | TOKENIZER_END: 302 | 303 | array_pop($tokens); 304 | 305 | return $tokens; 306 | } 307 | } 308 | --------------------------------------------------------------------------------