├── src ├── Handlers │ ├── Handler.php │ └── AbstractTreeHandler.php ├── Traitor.php └── TraitUseAdder.php ├── composer.json ├── LICENSE └── README.md /src/Handlers/Handler.php: -------------------------------------------------------------------------------- 1 | 9 | * @link https://github.com/kkszymanowski/traitor 10 | * @license MIT 11 | */ 12 | 13 | namespace Traitor\Handlers; 14 | 15 | interface Handler 16 | { 17 | public function handle(); 18 | 19 | public function toString(); 20 | 21 | public function toArray(); 22 | } 23 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "kkszymanowski/traitor", 3 | "description": "Add a trait use statement to existing PHP class", 4 | "keywords": ["PHP", "trait", "add"], 5 | "license": "MIT", 6 | "support": { 7 | "issues": "https://github.com/kkszymanowski/traitor/issues", 8 | "source": "https://github.com/kkszymanowski/traitor", 9 | "email": "kuba.szymanowski@inf24.pl" 10 | }, 11 | "authors": [ 12 | { 13 | "name": "Kuba Szymanowski", 14 | "email": "kuba.szymanowski@inf24.pl" 15 | } 16 | ], 17 | "require": { 18 | "php": ">=5.4", 19 | "nikic/php-parser": "^1.0|^2.0|^3.0|^4.0|^5.0" 20 | }, 21 | "require-dev": { 22 | "phpunit/phpunit": "8.*" 23 | }, 24 | "autoload": { 25 | "psr-4": { 26 | "Traitor\\": "src/" 27 | } 28 | }, 29 | "autoload-dev": { 30 | "classmap": [ 31 | "tests/TestCase.php" 32 | ] 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Kuba Szymanowski 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/Traitor.php: -------------------------------------------------------------------------------- 1 | 9 | * @link https://github.com/kkszymanowski/traitor 10 | * @license MIT 11 | */ 12 | 13 | namespace Traitor; 14 | 15 | class Traitor 16 | { 17 | /** 18 | * @param string $trait 19 | * @return TraitUseAdder 20 | */ 21 | public static function addTrait($trait) 22 | { 23 | $instance = new TraitUseAdder(); 24 | 25 | return $instance->addTraits([$trait]); 26 | } 27 | 28 | /** 29 | * @param array $traits 30 | * @return TraitUseAdder 31 | */ 32 | public static function addTraits($traits) 33 | { 34 | $instance = new TraitUseAdder(); 35 | 36 | return $instance->addTraits($traits); 37 | } 38 | 39 | /** 40 | * Check if provided class uses a specific trait. 41 | * 42 | * @param string $className 43 | * @param string $traitName 44 | * @return bool 45 | */ 46 | public static function alreadyUses($className, $traitName) 47 | { 48 | return in_array($traitName, class_uses($className)); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/TraitUseAdder.php: -------------------------------------------------------------------------------- 1 | 9 | * @link https://github.com/kkszymanowski/traitor 10 | * @license MIT 11 | */ 12 | 13 | namespace Traitor; 14 | 15 | use BadMethodCallException; 16 | use ReflectionClass; 17 | use RuntimeException; 18 | use Traitor\Handlers\AbstractTreeHandler; 19 | 20 | class TraitUseAdder 21 | { 22 | /** @var array */ 23 | protected $traitReflections = []; 24 | 25 | /** 26 | * @param string $trait 27 | * @return static 28 | */ 29 | public function addTrait($trait) 30 | { 31 | return $this->addTraits([$trait]); 32 | } 33 | 34 | /** 35 | * @param array $traits 36 | * @return static 37 | */ 38 | public function addTraits(array $traits) 39 | { 40 | foreach ($traits as $trait) { 41 | $this->traitReflections[] = new ReflectionClass($trait); 42 | } 43 | 44 | return $this; 45 | } 46 | 47 | /** 48 | * @param string $class 49 | * @return $this 50 | * 51 | * @throws BadMethodCallException 52 | * @throws RuntimeException 53 | */ 54 | public function toClass($class) 55 | { 56 | if (count($this->traitReflections) == 0) { 57 | throw new BadMethodCallException("No traits to add were found. Call 'addTrait' first."); 58 | } 59 | 60 | $classReflection = new ReflectionClass($class); 61 | 62 | $filePath = $classReflection->getFileName(); 63 | 64 | $content = file($filePath); 65 | 66 | foreach ($this->traitReflections as $traitReflection) { 67 | $handler = new AbstractTreeHandler( 68 | $content, 69 | $traitReflection->getName(), 70 | $classReflection->getName() 71 | ); 72 | 73 | $content = $handler->handle()->toArray(); 74 | } 75 | 76 | file_put_contents($filePath, implode($content)); 77 | 78 | return $this; 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Traitor 2 | [![StyleCI](https://styleci.io/repos/60994435/shield?style=flat)](https://styleci.io/repos/60994435) 3 | [![Build Status](https://travis-ci.org/KKSzymanowski/Traitor.svg?branch=master)](https://travis-ci.org/KKSzymanowski/Traitor) 4 | [![Latest Stable Version](https://poser.pugx.org/kkszymanowski/traitor/v/stable)](https://packagist.org/packages/kkszymanowski/traitor) 5 | [![License](https://poser.pugx.org/kkszymanowski/traitor/license)](https://packagist.org/packages/kkszymanowski/traitor) 6 | 7 | A PHP package for automatically adding a `trait use statement` to a given class. 8 | 9 | ## Installation 10 | Via composer: 11 | ``` 12 | composer require kkszymanowski/traitor 13 | ``` 14 | 15 | ## Usage 16 | - Basic usage: 17 | ``` 18 | use Traitor\Traitor; 19 | 20 | Traitor::addTrait(FooTrait::class)->toClass(FooClass:class); 21 | ``` 22 | - Add multiple traits: 23 | ```php 24 | use Traitor\Traitor; 25 | 26 | Traitor::addTraits([ 27 | FooTrait::class, 28 | BarTrait::class, 29 | BazTrait::class 30 | ])->toClass(FooClass::class); 31 | 32 | //or 33 | 34 | Traitor::addTrait(FooTrait::class) 35 | ->addTrait(BarTrait::class) 36 | ->addTrait(BazTrait::class) 37 | ->toClass(FooClass::class); 38 | ``` 39 | - Check if class already uses trait: 40 | ```php 41 | use Traitor\Traitor; 42 | 43 | $alreadyUses = Traitor::alreadyUses(FooClass::class, BarTrait::class); 44 | ``` 45 | - Only generate output without changing files: 46 | ```php 47 | use Traitor\Handlers\AbstractTreeHandler; 48 | 49 | $handler = new AbstractTreeHandler(file($originalFilePath), FooTrait::class, BarClass::class); 50 | 51 | $newContent = $handler->handle()->toString(); 52 | ``` 53 | Note, that `AbstractTreeHandler` accepts input file as an array of lines, such as one produced from `file()` call. 54 | 55 | ## Behavior 56 | Adding a new trait use statement does not change in any way formatting of your file(or at least it shouldn't). 57 | 58 | If the trait is not present in the `use` section below the namespace declaration, it will be also added there, below any existing imports. 59 | 60 | If it's not present in the `use` section in the class body, it will be added there above first existing use statement, on it's own line: 61 | ``` 62 | use Bar\PreviouslyExistingTrait; 63 | use Baz\NewlyAddedTrait; // Here 64 | 65 | class Foo 66 | { 67 | use NewlyAddedTrait; // And here 68 | use PreviouslyExistingTrait; 69 | } 70 | ``` 71 | -------------------------------------------------------------------------------- /src/Handlers/AbstractTreeHandler.php: -------------------------------------------------------------------------------- 1 | 9 | * @link https://github.com/kkszymanowski/traitor 10 | * @license MIT 11 | */ 12 | 13 | namespace Traitor\Handlers; 14 | 15 | use Exception; 16 | use PhpParser\Error; 17 | use PhpParser\Lexer; 18 | use PhpParser\Node\Stmt\Class_; 19 | use PhpParser\Node\Stmt\Namespace_; 20 | use PhpParser\Node\Stmt\TraitUse; 21 | use PhpParser\Node\Stmt\Use_; 22 | 23 | class AbstractTreeHandler implements Handler 24 | { 25 | /** @var array */ 26 | protected $content; 27 | 28 | /** @var string */ 29 | protected $trait; 30 | 31 | /** @var string */ 32 | protected $traitShortName; 33 | 34 | /** @var string */ 35 | protected $class; 36 | 37 | /** @var string */ 38 | protected $classShortName; 39 | 40 | /** @var array */ 41 | protected $syntaxTree; 42 | 43 | /** @var Namespace_ */ 44 | protected $namespace; 45 | 46 | /** @var array */ 47 | protected $importStatements; 48 | 49 | /** @var array */ 50 | protected $classes; 51 | 52 | /** @var Class_ */ 53 | protected $classAbstractTree; 54 | 55 | /** @var string */ 56 | protected $lineEnding = "\n"; 57 | 58 | /** 59 | * @param array $content 60 | * @param string $trait 61 | * @param string $class 62 | */ 63 | public function __construct($content, $trait, $class) 64 | { 65 | $this->content = $content; 66 | 67 | $this->determineLineEnding(); 68 | 69 | $this->trait = $trait; 70 | $traitParts = explode('\\', $trait); 71 | $this->traitShortName = array_pop($traitParts); 72 | 73 | $this->class = $class; 74 | $classParts = explode('\\', $class); 75 | $this->classShortName = array_pop($classParts); 76 | } 77 | 78 | /** 79 | * @return $this 80 | */ 81 | public function handle() 82 | { 83 | $this->buildSyntaxTree() 84 | ->addTraitImport() 85 | ->buildSyntaxTree() 86 | ->addTraitUseStatement(); 87 | 88 | return $this; 89 | } 90 | 91 | /** 92 | * @return string 93 | */ 94 | public function toString() 95 | { 96 | return implode($this->content); 97 | } 98 | 99 | /** 100 | * @return array 101 | */ 102 | public function toArray() 103 | { 104 | return $this->content; 105 | } 106 | 107 | /** 108 | * @return $this 109 | * 110 | * @throws Exception 111 | */ 112 | protected function buildSyntaxTree() 113 | { 114 | $this->parseContent() 115 | ->retrieveNamespace() 116 | ->retrieveImports() 117 | ->retrieveClasses() 118 | ->findClassDefinition(); 119 | 120 | return $this; 121 | } 122 | 123 | /** 124 | * @return $this 125 | */ 126 | protected function addTraitImport() 127 | { 128 | if ($this->hasTraitImport()) { 129 | return $this; 130 | } 131 | 132 | $lastImport = $this->getLastImport(); 133 | if ($lastImport === false) { 134 | $lineNumber = $this->classAbstractTree->getLine() - 1; 135 | $newImport = 'use '.$this->trait.';'.$this->lineEnding; 136 | 137 | array_splice($this->content, $lineNumber, 0, $this->lineEnding); 138 | } else { 139 | $lineNumber = $this->getLastImport()->getAttribute('endLine'); 140 | $newImport = 'use '.$this->trait.';'.$this->lineEnding; 141 | } 142 | 143 | array_splice($this->content, $lineNumber, 0, $newImport); 144 | 145 | return $this; 146 | } 147 | 148 | /** 149 | * @return $this 150 | */ 151 | protected function addTraitUseStatement() 152 | { 153 | if ($this->alreadyUsesTrait()) { 154 | return $this; 155 | } 156 | 157 | $this->openBracketsIfNecessary(); 158 | 159 | $line = $this->getNewTraitUseLine(); 160 | 161 | $newTraitUse = static::getIndentation($this->content[$line]).'use '.$this->traitShortName.';'.$this->lineEnding; 162 | 163 | array_splice($this->content, $line, 0, $newTraitUse); 164 | 165 | return $this; 166 | } 167 | 168 | /** 169 | * @return $this 170 | * 171 | * @throws Exception 172 | */ 173 | protected function parseContent() 174 | { 175 | $flatContent = implode($this->content); 176 | 177 | try { 178 | $parser = $this->getParser(); 179 | $this->syntaxTree = $parser->parse($flatContent); 180 | } catch (Error $e) { 181 | throw new Exception('Error on parsing '.$this->classShortName." class\n".$e->getMessage()); 182 | } 183 | 184 | return $this; 185 | } 186 | 187 | /** 188 | * @return $this 189 | * 190 | * @throws Exception 191 | */ 192 | protected function retrieveNamespace() 193 | { 194 | $namespaceNode = null; 195 | foreach ($this->syntaxTree as $item) { 196 | if ($item instanceof Namespace_) { 197 | $namespaceNode = $item; 198 | } 199 | } 200 | 201 | if (! $namespaceNode) { 202 | // var_dump($this->syntaxTree);die; 203 | // throw new Exception("Could not locate namespace definition for class '".$this->classShortName."'"); 204 | } 205 | 206 | $this->namespace = $namespaceNode; 207 | 208 | return $this; 209 | } 210 | 211 | /** 212 | * @return $this 213 | */ 214 | protected function retrieveImports() 215 | { 216 | if ($this->namespace === null) { 217 | $this->importStatements = array_filter($this->syntaxTree, function ($statement) { 218 | return $statement instanceof Use_; 219 | }); 220 | } else { 221 | $this->importStatements = array_filter($this->namespace->stmts, function ($statement) { 222 | return $statement instanceof Use_; 223 | }); 224 | } 225 | 226 | return $this; 227 | } 228 | 229 | /** 230 | * @return $this 231 | */ 232 | protected function retrieveClasses() 233 | { 234 | if ($this->namespace === null) { 235 | $this->classes = array_filter($this->syntaxTree, function ($statement) { 236 | return $statement instanceof Class_; 237 | }); 238 | } else { 239 | $this->classes = array_filter($this->namespace->stmts, function ($statement) { 240 | return $statement instanceof Class_; 241 | }); 242 | } 243 | 244 | return $this; 245 | } 246 | 247 | /** 248 | * @return \PhpParser\Node\Stmt\Use_ 249 | */ 250 | protected function getLastImport() 251 | { 252 | return end($this->importStatements); 253 | } 254 | 255 | /** 256 | * @return bool 257 | */ 258 | protected function hasTraitImport() 259 | { 260 | foreach ($this->importStatements as $statement) { 261 | if ($statement->uses[0]->name->toString() == $this->trait) { 262 | return true; 263 | } 264 | } 265 | 266 | return false; 267 | } 268 | 269 | /** 270 | * @return bool 271 | */ 272 | protected function alreadyUsesTrait() 273 | { 274 | $traitUses = array_filter($this->classAbstractTree->stmts, function ($statement) { 275 | return $statement instanceof TraitUse; 276 | }); 277 | 278 | /** @var TraitUse $statement */ 279 | foreach ($traitUses as $statement) { 280 | foreach ($statement->traits as $traitUse) { 281 | if ($traitUse->toString() == $this->trait 282 | || $traitUse->toString() == $this->traitShortName 283 | ) { 284 | return true; 285 | } 286 | } 287 | } 288 | 289 | return false; 290 | } 291 | 292 | /** 293 | * @return $this 294 | * 295 | * @throws Exception 296 | */ 297 | protected function findClassDefinition() 298 | { 299 | foreach ($this->classes as $class) { 300 | if ($class->name == $this->classShortName) { 301 | $this->classAbstractTree = $class; 302 | 303 | return $this; 304 | } 305 | } 306 | 307 | throw new Exception('Class '.$this->classShortName.' not found'); 308 | } 309 | 310 | /** 311 | * @return int 312 | * 313 | * @throws Exception 314 | */ 315 | protected function getNewTraitUseLine() 316 | { 317 | // If the first statement is a trait use, insert the new trait use before it. 318 | if (isset($this->classAbstractTree->stmts[0])) { 319 | $firstStatement = $this->classAbstractTree->stmts[0]; 320 | 321 | if ($firstStatement instanceof TraitUse) { 322 | return $firstStatement->getLine() - 1; 323 | } 324 | } 325 | 326 | // If the first statement is not a trait use, insert the new one just after the opening bracket. 327 | for ($line = $this->classAbstractTree->getLine() - 1; $line < count($this->content); $line++) { 328 | if (strpos($this->content[$line], '{') !== false) { 329 | return $line + 1; 330 | } 331 | } 332 | 333 | throw new Exception("Opening bracket not found in class [$this->classShortName]"); 334 | } 335 | 336 | protected function openBracketsIfNecessary() 337 | { 338 | for ($line = $this->classAbstractTree->getLine() - 1; $line < count($this->content); $line++) { 339 | $trimmedLine = rtrim($this->content[$line]); 340 | 341 | if (substr($trimmedLine, strlen($trimmedLine) - 2) == '{}') { 342 | $trimmedLine = rtrim(substr($trimmedLine, 0, strlen($trimmedLine) - 2)); 343 | if (strlen($trimmedLine) == 0) { 344 | $this->content[$line] = '{'.$this->lineEnding; 345 | array_splice($this->content, $line + 1, 0, '}'.$this->lineEnding); 346 | } else { 347 | $this->content[$line] = $trimmedLine.$this->lineEnding; 348 | array_splice($this->content, $line + 1, 0, '{'.$this->lineEnding); 349 | array_splice($this->content, $line + 2, 0, '}'.$this->lineEnding); 350 | } 351 | 352 | $this->buildSyntaxTree(); 353 | break; 354 | } 355 | } 356 | } 357 | 358 | /** 359 | * Default line ending is set to LF. 360 | * 361 | * If there is at least one line in the provided file 362 | * and it contains CR+LF, change line ending CR+LF. 363 | * 364 | * @return $this 365 | */ 366 | protected function determineLineEnding() 367 | { 368 | if (isset($this->content[0]) && strpos($this->content[0], "\r\n") !== false) { 369 | $this->lineEnding = "\r\n"; 370 | } 371 | 372 | return $this; 373 | } 374 | 375 | /** 376 | * @param $line 377 | * @return string 378 | */ 379 | protected static function getIndentation($line) 380 | { 381 | preg_match('/^\s*/', $line, $match); 382 | 383 | if (isset($match[0])) { 384 | $match[0] = trim($match[0], "\n\r"); 385 | 386 | if (strlen($match[0]) > 0) { 387 | return $match[0]; 388 | } 389 | } 390 | 391 | return str_repeat(' ', 4); 392 | } 393 | 394 | protected function getParser() 395 | { 396 | $refParser = new \ReflectionClass('\PhpParser\Parser'); 397 | 398 | if (! $refParser->isInterface()) { 399 | // If we are running nikic/php-parser 1.* 400 | return new \PhpParser\Parser(new Lexer()); 401 | } else { 402 | $parserFactory = new \PhpParser\ParserFactory(); 403 | 404 | if (method_exists($parserFactory, 'createForHostVersion')) { 405 | return $parserFactory->createForHostVersion(); 406 | } elseif (method_exists($parserFactory, 'create')) { 407 | return $parserFactory->create(\PhpParser\ParserFactory::PREFER_PHP7); 408 | } else { 409 | throw new \RuntimeException('Unsupported version of nikic/php-parser'); 410 | } 411 | } 412 | } 413 | } 414 | --------------------------------------------------------------------------------