├── .gitignore ├── tests ├── Fixtures │ ├── TabCompletion │ │ ├── Qux.php │ │ ├── functions.php │ │ ├── Quux.php │ │ ├── Baz.php │ │ ├── Bar.php │ │ └── Foo.php │ └── Visitor │ │ └── Console │ │ ├── 5.4.php │ │ ├── custom.php │ │ ├── 3.4.php │ │ └── 4.4.php ├── Util │ └── NodeVisitorTest.php ├── Parser │ └── SqlParserTest.php └── TabCompletion │ └── AutoCompleterTest.php ├── .php-cs-fixer.php ├── src ├── TabCompletion │ ├── NonCombinableMatcher.php │ ├── AutoCompleter.php │ └── Matcher │ │ ├── SqlDqlMatcher.php │ │ └── MethodChainingMatcher.php ├── Shell.php ├── Shell7.php ├── include.php ├── ShellTrait.php ├── bootstrap.php ├── generate.php ├── SFChecker.php ├── Output │ └── Paint.php ├── Parser │ ├── NodeVisitor.php │ ├── DocParser.php │ ├── SqlParser.php │ └── Reflection.php ├── Util │ ├── Helper.php │ ├── DoctrineProxy.php │ └── DoctrineEM.php ├── functions.php ├── Formatter │ └── SignatureFormatter.php ├── Command │ └── ListEntitiesCommand.php └── SFLoader.php ├── phpunit.xml.dist ├── LICENSE ├── composer.json ├── bin └── psym └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | .php-cs-fixer.cache 2 | .phpunit.result.cache 3 | composer.lock 4 | composer-sf.json 5 | composer-sf.lock 6 | composer-sf7.json 7 | composer-sf7.lock 8 | vendor/ 9 | vendor-sf/ 10 | vendor-sf7/ 11 | -------------------------------------------------------------------------------- /tests/Fixtures/TabCompletion/Qux.php: -------------------------------------------------------------------------------- 1 | in(['bin', 'src']) 5 | ; 6 | 7 | $config = new PhpCsFixer\Config(); 8 | return $config->setRules([ 9 | '@Symfony' => true, 10 | ]) 11 | ->setFinder($finder) 12 | ; 13 | -------------------------------------------------------------------------------- /src/TabCompletion/NonCombinableMatcher.php: -------------------------------------------------------------------------------- 1 | 5 | 6 | 7 | tests/ 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /tests/Fixtures/TabCompletion/functions.php: -------------------------------------------------------------------------------- 1 | getKernel()->getContainer()->has('services_resetter')) { 20 | $this->getKernel()->getContainer()->get('services_resetter')->reset(); 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/Shell7.php: -------------------------------------------------------------------------------- 1 | getKernel()->getContainer()->has('services_resetter')) { 20 | $this->getKernel()->getContainer()->get('services_resetter')->reset(); 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/include.php: -------------------------------------------------------------------------------- 1 | isSymfony7()) { 13 | require __DIR__.'/../vendor-sf7/autoload.php'; 14 | } else { 15 | require __DIR__.'/../vendor-sf/autoload.php'; 16 | } 17 | 18 | return (new \TareqAS\Psym\SFLoader(getcwd()))->getUsefulServices(); 19 | -------------------------------------------------------------------------------- /src/ShellTrait.php: -------------------------------------------------------------------------------- 1 | kernel) { 19 | throw new \RuntimeException('Kernel is not set yet!'); 20 | } 21 | 22 | return $this->kernel; 23 | } 24 | 25 | public function setKernel($kernel) 26 | { 27 | $this->kernel = $kernel; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /tests/Fixtures/Visitor/Console/5.4.php: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env php 2 | $v) { 7 | $_ENV[$k] = $_ENV[$k] ?? (isset($_SERVER[$k]) && 0 !== strpos($k, 'HTTP_') ? $_SERVER[$k] : $v); 8 | } 9 | } elseif (!class_exists(Dotenv::class)) { 10 | throw new RuntimeException('Please run "composer require symfony/dotenv" to load the ".env" files configuring the application.'); 11 | } else { 12 | (new Dotenv(false))->loadEnv(getcwd().'/.env'); 13 | } 14 | 15 | $_SERVER += $_ENV; 16 | $_SERVER['APP_ENV'] = $_ENV['APP_ENV'] = ($_SERVER['APP_ENV'] ?? $_ENV['APP_ENV'] ?? null) ?: 'dev'; 17 | $_SERVER['APP_DEBUG'] = $_SERVER['APP_DEBUG'] ?? $_ENV['APP_DEBUG'] ?? 'prod' !== $_SERVER['APP_ENV']; 18 | $_SERVER['APP_DEBUG'] = $_ENV['APP_DEBUG'] = (int) $_SERVER['APP_DEBUG'] || filter_var($_SERVER['APP_DEBUG'], FILTER_VALIDATE_BOOLEAN) ? '1' : '0'; 19 | -------------------------------------------------------------------------------- /tests/Fixtures/Visitor/Console/3.4.php: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env php 2 | getParameterOption(['--env', '-e'], getenv('SYMFONY_ENV') ?: 'dev', true); 19 | $debug = getenv('SYMFONY_DEBUG') !== '0' && !$input->hasParameterOption('--no-debug', true) && $env !== 'prod'; 20 | 21 | if ($debug) { 22 | Debug::enable(); 23 | } 24 | 25 | $kernel = new AppKernel($env, $debug); 26 | $application = new Application($kernel); 27 | $application->run($input); 28 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Tareq Ahamed 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 | -------------------------------------------------------------------------------- /tests/Fixtures/TabCompletion/Foo.php: -------------------------------------------------------------------------------- 1 | _bar = new Bar(); 17 | $this->_baz = new Baz(); 18 | } 19 | 20 | /** 21 | * @return self 22 | */ 23 | public static function init() 24 | { 25 | return new self(); 26 | } 27 | 28 | /** 29 | * A sample doc description 30 | * 31 | * @return Bar 32 | */ 33 | public function bar() 34 | { 35 | return $this->_bar; 36 | } 37 | 38 | public function baz(): Baz 39 | { 40 | return $this->_baz; 41 | } 42 | 43 | public function union(): Bar|Baz 44 | { 45 | } 46 | 47 | /** 48 | * @return Bar|Baz 49 | */ 50 | public function unionDoc() 51 | { 52 | } 53 | 54 | public function intersection(): Bar&Baz 55 | { 56 | } 57 | 58 | /** 59 | * @return Bar&Baz 60 | */ 61 | public function intersectionDoc() 62 | { 63 | } 64 | 65 | /** 66 | * it has no return type 67 | */ 68 | public function noReturn() 69 | { 70 | } 71 | } 72 | 73 | function funcFoo(): Foo 74 | { 75 | return new Foo(); 76 | } 77 | -------------------------------------------------------------------------------- /src/generate.php: -------------------------------------------------------------------------------- 1 | '*', 10 | 'symfony/var-dumper' => '*', 11 | ]; 12 | 13 | $sfJson = [ 14 | 'autoload' => $composer['autoload'], 15 | 'require' => $composer['require'], 16 | 'replace' => $replace, 17 | 'config' => [ 18 | 'platform' => ['php' => '7.2.99'], 19 | 'vendor-dir' => 'vendor-sf', 20 | ], 21 | ]; 22 | 23 | $sf7Json = [ 24 | 'autoload' => $composer['autoload'], 25 | 'require' => array_replace($composer['require'], ['psy/psysh' => 'dev-main']), 26 | 'replace' => $replace, 27 | 'config' => [ 28 | 'platform' => ['php' => '8.2.99'], 29 | 'vendor-dir' => 'vendor-sf7', 30 | ], 31 | 'repositories' => [ 32 | [ 33 | 'type' => 'vcs', 34 | 'url' => 'https://github.com/tareqas/psysh', 35 | ], 36 | ], 37 | ]; 38 | 39 | file_put_contents($sfFile, json_encode($sfJson, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES)); 40 | echo "composer-sf.json has been created successfully.\n"; 41 | file_put_contents($sf7File, json_encode($sf7Json, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES)); 42 | echo "composer-sf7.json has been created successfully.\n"; 43 | -------------------------------------------------------------------------------- /tests/Fixtures/Visitor/Console/4.4.php: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env php 2 | getParameterOption(['--env', '-e'], null, true)) { 23 | putenv('APP_ENV='.$_SERVER['APP_ENV'] = $_ENV['APP_ENV'] = $env); 24 | } 25 | 26 | if ($input->hasParameterOption('--no-debug', true)) { 27 | putenv('APP_DEBUG='.$_SERVER['APP_DEBUG'] = $_ENV['APP_DEBUG'] = '0'); 28 | } 29 | 30 | require dirname(__DIR__).'/config/bootstrap.php'; 31 | 32 | if ($_SERVER['APP_DEBUG']) { 33 | umask(0000); 34 | 35 | if (class_exists(Debug::class)) { 36 | Debug::enable(); 37 | } 38 | } 39 | 40 | $kernel = new Kernel($_SERVER['APP_ENV'], (bool) $_SERVER['APP_DEBUG']); 41 | $application = new Application($kernel); 42 | $application->run($input); 43 | -------------------------------------------------------------------------------- /src/TabCompletion/AutoCompleter.php: -------------------------------------------------------------------------------- 1 | matchers as $matcher) { 28 | if ($matcher->hasMatched($tokens)) { 29 | $foundMatches = $matcher->getMatches($tokens, $info); 30 | 31 | if ($foundMatches && $matcher instanceof NonCombinableMatcher) { 32 | return $foundMatches; 33 | } 34 | 35 | $matches = \array_merge($foundMatches, $matches); 36 | } 37 | } 38 | 39 | $matches = \array_unique($matches); 40 | 41 | return !empty($matches) ? $matches : ['']; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /tests/Util/NodeVisitorTest.php: -------------------------------------------------------------------------------- 1 | parser = (new ParserFactory())->createForNewestSupportedVersion(); 19 | $this->traverser = new NodeTraverser(); 20 | $this->nodeVisitor = new NodeVisitor(); 21 | $this->traverser->addVisitor($this->nodeVisitor); 22 | } 23 | 24 | /** 25 | * @dataProvider getSymfonyVersions 26 | */ 27 | public function testToGetKernelClass($version, $kernel, $doesItReturnClosure) 28 | { 29 | $code = file_get_contents(__DIR__."/../Fixtures/Visitor/Console/$version.php"); 30 | 31 | $ast = $this->parser->parse($code); 32 | $this->traverser->traverse($ast); 33 | 34 | $this->assertSame($kernel, $this->nodeVisitor->kernelClass); 35 | $this->assertSame($doesItReturnClosure, $this->nodeVisitor->doesItReturnClosure); 36 | } 37 | 38 | private function getSymfonyVersions(): array 39 | { 40 | return [ 41 | ['3.4', 'AppKernel', false], 42 | ['4.4', 'App\Kernel', false], 43 | ['5.4', 'App\Kernel', true], 44 | ['custom', 'MyApp\CustomKernel', true], 45 | ]; 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/SFChecker.php: -------------------------------------------------------------------------------- 1 | projectDir = $projectDir; 13 | } 14 | 15 | public function getComposer(): array 16 | { 17 | if ($this->composer) { 18 | return $this->composer; 19 | } 20 | 21 | if (!file_exists($composer = $this->projectDir.'/composer.json')) { 22 | return []; 23 | } 24 | 25 | return $this->composer = json_decode(file_get_contents($composer), true); 26 | } 27 | 28 | public function isSymfonyApp(): bool 29 | { 30 | if (!$composer = $this->getComposer()) { 31 | return false; 32 | } 33 | 34 | if (!isset($composer['require']['symfony/framework-bundle'])) { 35 | return false; 36 | } 37 | 38 | return true; 39 | } 40 | 41 | public function isSymfony7(): bool 42 | { 43 | if (!$this->isSymfonyApp()) { 44 | return false; 45 | } 46 | 47 | $composer = $this->getComposer(); 48 | 49 | if (isset($composer['require']['symfony/console']) && version_compare($composer['require']['symfony/console'], '7.0', '>=')) { 50 | return true; 51 | } 52 | 53 | if (isset($composer['extra']['symfony']['require']) && version_compare($composer['extra']['symfony']['require'], '7.0', '>=')) { 54 | return true; 55 | } 56 | 57 | return false; 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "tareqas/psym", 3 | "description": "A REPL for Symfony and PHP", 4 | "type": "library", 5 | "keywords": ["symfony", "repl", "console", "shell", "interactive", "psym", "psysh"], 6 | "license": "MIT", 7 | "authors": [ 8 | { 9 | "name": "Tareq Ahamed", 10 | "email": "tareq.ahamed@outlook.com" 11 | } 12 | ], 13 | "bin": ["bin/psym"], 14 | "autoload": { 15 | "psr-4": { 16 | "TareqAS\\Psym\\": "src/" 17 | }, 18 | "files": [ 19 | "src/functions.php" 20 | ] 21 | }, 22 | "autoload-dev": { 23 | "psr-4": { 24 | "TareqAS\\Psym\\Tests\\": "tests/" 25 | }, 26 | "files": [ 27 | "tests/Fixtures/TabCompletion/functions.php" 28 | ] 29 | }, 30 | "require": { 31 | "php": ">=7.2", 32 | "ext-json": "*", 33 | "ext-tokenizer": "*", 34 | "jetbrains/phpstorm-stubs": "@dev", 35 | "phpstan/phpdoc-parser": "^1.30", 36 | "psy/psysh": "^0.11" 37 | }, 38 | "require-dev": { 39 | "doctrine/orm": "^2.19", 40 | "friendsofphp/php-cs-fixer": "^3.4", 41 | "mockery/mockery": "^1.3", 42 | "phpunit/phpunit": "^8.0", 43 | "symfony/framework-bundle": "^5.4", 44 | "symfony/runtime": "^5.4" 45 | }, 46 | "config": { 47 | "sort-packages": true, 48 | "platform": { 49 | "php": "7.2.99" 50 | }, 51 | "allow-plugins": { 52 | "symfony/runtime": false 53 | } 54 | }, 55 | "scripts": { 56 | "cs-fixer": "@php vendor/bin/php-cs-fixer fix --dry-run --diff", 57 | "cs-fixer-run": "@php vendor/bin/php-cs-fixer fix", 58 | "test": "@php vendor/bin/phpunit", 59 | "post-install-cmd": [ 60 | "@php src/generate.php", 61 | "@putenv COMPOSER=composer-sf.json", 62 | "@composer install", 63 | "@putenv COMPOSER=composer-sf7.json", 64 | "@composer install" 65 | ], 66 | "post-update-cmd": [ 67 | "@php src/generate.php", 68 | "@putenv COMPOSER=composer-sf.json", 69 | "@composer update", 70 | "@putenv COMPOSER=composer-sf7.json", 71 | "@composer update" 72 | ] 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /bin/psym: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env php 2 | isSymfony7() ? $autoloaderSf7File : $autoloaderSfFile; 26 | } else { 27 | $autoloaderDefault = require $autoloaderDefaultFile; 28 | } 29 | 30 | $shell = function () { 31 | global $sfChecker; 32 | $config = new \Psy\Configuration(); 33 | $config->setAutoCompleter(new \TareqAS\Psym\TabCompletion\AutoCompleter()); 34 | $class = $sfChecker->isSymfony7() ? \TareqAS\Psym\Shell7::class : \TareqAS\Psym\Shell::class; 35 | return new $class($config); 36 | }; 37 | 38 | $sfLoader = new \TareqAS\Psym\SFLoader(getcwd()); 39 | 40 | if ($sfChecker->isSymfonyApp() && $sfLoader->getKernelInstance()) { 41 | [$kernel, $container, $doctrine, $em] = $sfLoader->getUsefulServices(); 42 | $commands = $sfLoader->getAllCommands(); 43 | array_unshift($commands, new \TareqAS\Psym\Command\ListEntitiesCommand()); 44 | 45 | $sh = $shell(); 46 | $sh->setKernel($kernel); 47 | $sh->setScopeVariables(compact('kernel', 'container', 'doctrine', 'em')); 48 | $sh->addCommands($commands); 49 | $sh->addMatchers([ 50 | new \TareqAS\Psym\TabCompletion\Matcher\SqlDqlMatcher(), 51 | new \TareqAS\Psym\TabCompletion\Matcher\MethodChainingMatcher(), 52 | ]); 53 | $sh->run(); 54 | } else { 55 | if (isset($autoloaderUser, $autoloaderSf)) { 56 | $autoloaderUser->unregister(); 57 | $autoloaderSf->unregister(); 58 | } 59 | 60 | if (!isset($autoloaderDefault)) { 61 | require $autoloaderDefaultFile; 62 | } 63 | 64 | $sh = $shell(); 65 | $sh->addMatchers([ 66 | new \TareqAS\Psym\TabCompletion\Matcher\MethodChainingMatcher(), 67 | ]); 68 | $sh->run(); 69 | } 70 | -------------------------------------------------------------------------------- /src/Output/Paint.php: -------------------------------------------------------------------------------- 1 | new OutputFormatterStyle('green', null, ['bold']), 20 | 'i' => new OutputFormatterStyle('green', null, ['underscore']), 21 | 'em' => new OutputFormatterStyle('green', null, ['bold']), 22 | 'pre' => new OutputFormatterStyle('green', null, ['bold']), 23 | 'code' => new OutputFormatterStyle('green', null, ['bold']), 24 | 'doc' => new OutputFormatterStyle('green'), 25 | 'doc-tag' => new OutputFormatterStyle('magenta'), 26 | 'keyword' => new OutputFormatterStyle('cyan'), 27 | 'name' => new OutputFormatterStyle('red'), 28 | 'search' => new OutputFormatterStyle('red'), 29 | ]); 30 | } 31 | 32 | public static function docAndSignature(string $doc, string $signature): array 33 | { 34 | $formatter = self::getFormatter(); 35 | $doc = self::sanitizePhpStormDoc($doc); 36 | $doc = $formatter->format("$doc"); 37 | $doc = html_entity_decode($doc); 38 | $signature = $formatter->format($signature); 39 | 40 | return ["$doc\n$signature\n", '']; 41 | } 42 | 43 | public static function message(string $message): array 44 | { 45 | $message = self::getFormatter()->format($message); 46 | 47 | return ["\n$message\n", '']; 48 | } 49 | 50 | private static function sanitizePhpStormDoc(string $doc): string 51 | { 52 | $doc = preg_replace('#
|

|

#i', "\n * ", $doc); 53 | 54 | $doc = preg_replace('##i', "\n * ", $doc); 55 | $doc = preg_replace('#
  • #i', '— ', $doc); 56 | $doc = preg_replace('#
  • #i', '', $doc); 57 | 58 | $doc = preg_replace('#(\s+\*\s+)|(\s+\*\s+)#i', '', $doc); 59 | $doc = preg_replace('#\s+\*\s+#i', ' — ', $doc); 60 | 61 | $doc = preg_replace('#(?]+>#i', '', $doc); 62 | 63 | $doc = preg_replace('#```(.+?)```#s', '$1', $doc); 64 | $doc = preg_replace('#(@\w+)#', '$1', $doc); 65 | $doc = preg_replace('#(\$\w+)#', '$1', $doc); 66 | 67 | $doc = preg_replace('#(\n (\* +)(?!\S+)){2,}#', "\n * ", $doc); 68 | 69 | return trim($doc); 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/TabCompletion/Matcher/SqlDqlMatcher.php: -------------------------------------------------------------------------------- 1 | getSuggestions($functionName[1], $rawSql, $info); 37 | } 38 | 39 | private function getSuggestions(string $type, string $rawSql, array $info): array 40 | { 41 | $suggestions = []; 42 | $sqlParser = new SqlParser($rawSql, $type); 43 | $found = $sqlParser->askingFor($info); 44 | 45 | switch ($found['type']) { 46 | case 'TABLE': 47 | $tables = 'dql' === $sqlParser->type ? DoctrineEM::getEntities() : DoctrineEM::getTables(); 48 | if ($found['name']) { 49 | $suggestions = Helper::partialSearch($tables, $found['name']); 50 | } else { 51 | $suggestions = $tables; 52 | } 53 | break; 54 | case 'COLUMN': 55 | if (!$table = $sqlParser->findByTableOrAlias($found['alias'])) { 56 | $suggestions = SqlParser::$keywords; 57 | break; 58 | } 59 | $properties = 'dql' === $sqlParser->type ? DoctrineEM::getProperties($table['table']) : DoctrineEM::getColumns($table['table']); 60 | $properties = $found['name'] ? Helper::partialSearch($properties, $found['name']) : $properties; 61 | $suggestions = array_map(function ($property) use ($found) { 62 | return "{$found['alias']}.$property"; 63 | }, $properties); 64 | break; 65 | case 'KEYWORDS': 66 | if ($found['name']) { 67 | $suggestions = Helper::partialSearch(SqlParser::$keywords, $found['name']); 68 | } else { 69 | $suggestions = SqlParser::$keywords; 70 | } 71 | break; 72 | case 'KEYWORDS_STARTING': 73 | $suggestions = SqlParser::$keywordsStarting; 74 | break; 75 | } 76 | 77 | return $suggestions; 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /tests/Parser/SqlParserTest.php: -------------------------------------------------------------------------------- 1 | 'table1', 'alias' => 't1'], 35 | ['table' => 'table2', 'alias' => 't2'], 36 | // ['table' => 'table3', 'alias' => 't3'], 37 | // ['table' => 'table4', 'alias' => ''], 38 | ['table' => 'table5', 'alias' => 't5'], 39 | ['table' => 'table6', 'alias' => 't6'], 40 | ['table' => 'table7', 'alias' => 't7'], 41 | ]; 42 | 43 | $parser = new SqlParser($sql, 'sql'); 44 | $tables = $parser->getTables(); 45 | 46 | self::assertSame($expected, $tables); 47 | } 48 | 49 | public function testParserForDQL() 50 | { 51 | $sql = << 'App\Entity\Table1', 'alias' => 't1'], 60 | ['table' => 'App\Entity\Table2', 'alias' => 't2'], 61 | ['table' => 'App\Entity\Table3', 'alias' => 't3'], 62 | ]; 63 | 64 | $getEntitiesMappings = [ 65 | 'App\Entity\Table1' => [ 66 | 'table' => 'table1', 67 | 'properties' => [ 68 | 'c1' => ['column' => 'c1', 'targetEntity' => '', 'type' => 'string', 'default' => null], 69 | 'c2' => ['column' => 'c2', 'targetEntity' => 'App\Entity\Table2', 'type' => 'string', 'default' => null], 70 | 'c3' => ['column' => 'c3', 'targetEntity' => 'App\Entity\Table3', 'type' => 'string', 'default' => null], 71 | ], 72 | ], 73 | 'App\Entity\Table2' => [ 74 | 'table' => 'table2', 75 | 'properties' => [ 76 | 'c1' => ['column' => 'c1', 'targetEntity' => '', 'type' => 'string', 'default' => null], 77 | 'c2' => ['column' => 'c2', 'targetEntity' => '', 'type' => 'string', 'default' => null], 78 | ], 79 | ], 80 | 'App\Entity\Table3' => [ 81 | 'table' => 'table3', 82 | 'properties' => [], 83 | ], 84 | ]; 85 | 86 | $mock = Mockery::mock('alias:'.DoctrineEM::class); 87 | $mock->shouldReceive('findEntityFullName') 88 | ->andReturn('App\Entity\Table1'); 89 | $mock->shouldReceive('getEntitiesMappings') 90 | ->andReturn($getEntitiesMappings); 91 | 92 | $parser = new SqlParser($sql, 'dql'); 93 | $tables = $parser->getTables(); 94 | 95 | self::assertSame($expected, $tables); 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /src/Parser/NodeVisitor.php: -------------------------------------------------------------------------------- 1 | expr instanceof Node\Expr\Closure) { 23 | $this->doesItReturnClosure = true; 24 | } 25 | } 26 | 27 | public function enterNode(Node $node) 28 | { 29 | if ($node instanceof Node\Stmt\Namespace_ && $node->name instanceof Node\Name) { 30 | $this->namespace = $node->name->toString(); 31 | } 32 | 33 | if ($node instanceof Node\Stmt\UseUse) { 34 | $this->usedClasses[] = $node->name->toString(); 35 | } 36 | 37 | if ($node instanceof Node\Stmt\Class_ && $node->name instanceof Node\Identifier) { 38 | $class = $node->name->toString(); 39 | $this->className = $this->namespace ? $this->namespace.'\\'.$class : $class; 40 | } 41 | 42 | if ($node instanceof Node\Stmt\Function_ && $node->name instanceof Node\Identifier) { 43 | $functionName = $node->name->toString(); 44 | $this->docComments[$functionName] = $this->getDocComment($node); 45 | } 46 | 47 | if ($node instanceof Node\Stmt\Property) { 48 | $propertyName = $node->props[0]->name->toString(); 49 | $this->docComments[$propertyName] = $this->getDocComment($node); 50 | } 51 | 52 | if ($node instanceof Node\Stmt\ClassMethod) { 53 | $className = $node->name->toString(); 54 | $this->docComments[$className] = $this->getDocComment($node); 55 | } 56 | 57 | if ($node instanceof Node\Expr\Assign && $node->var instanceof Node\Expr\Variable && $node->expr instanceof Node\Expr\New_) { 58 | $this->variables[$node->var->name] = $node->expr->class->toString(); 59 | } 60 | 61 | if ($node instanceof Node\Expr\New_ && $node->class instanceof Node\Name) { 62 | $name = $node->class->toString(); 63 | if ('Symfony\Bundle\FrameworkBundle\Console\Application' === $this->getFullClassName($name)) { 64 | $kernelVar = $node->getArgs()[0]->value->name; 65 | $kernelName = $this->variables[$kernelVar] ?? null; 66 | $this->kernelClass = $kernelName ? $this->getFullClassName($kernelName) : null; 67 | } 68 | } 69 | } 70 | 71 | public function getFullClassName(string $class): string 72 | { 73 | $filter = array_filter($this->usedClasses, function ($usedClass) use ($class) { 74 | return substr($usedClass, -strlen($class)) === $class; 75 | }); 76 | 77 | if (!$filter && class_exists($className = $this->namespace ? $this->namespace.'\\'.$class : $class)) { 78 | $class = $className; 79 | } 80 | 81 | return reset($filter) ?: $class; 82 | } 83 | 84 | private function getDocComment($node): string 85 | { 86 | if ($comment = $node->getDocComment()) { 87 | return $comment->getText(); 88 | } 89 | 90 | return ''; 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /src/Parser/DocParser.php: -------------------------------------------------------------------------------- 1 | true, 'indexes' => true]; 29 | $constExprParser = new ConstExprParser(true, true, $usedAttributes); 30 | $typeParser = new TypeParser($constExprParser, true, $usedAttributes); 31 | self::$docParser = new PhpDocParser($typeParser, $constExprParser, true, true, $usedAttributes); 32 | } 33 | 34 | $lexer = new Lexer(); 35 | $this->tokens = new TokenIterator($lexer->tokenize($doc)); 36 | $this->docNode = self::$docParser->parse($this->tokens); 37 | 38 | $cloningTraverser = new NodeTraverser([new CloningVisitor()]); 39 | /** @var PhpDocNode $newPhpDocNode newDocNode */ 40 | [$newPhpDocNode] = $cloningTraverser->traverse([$this->docNode]); 41 | $this->newDocNode = $newPhpDocNode; 42 | } 43 | 44 | public static function parse(string $doc): self 45 | { 46 | return new self($doc ?: '/** */'); 47 | } 48 | 49 | public function getValue(string $tagName): string 50 | { 51 | if ($tag = $this->newDocNode->getTagsByName($tagName)) { 52 | $tag = array_shift($tag); 53 | 54 | return $tag->value->value; 55 | } 56 | 57 | return ''; 58 | } 59 | 60 | public function getPropertyType(): ?TypeNode 61 | { 62 | if ($var = $this->newDocNode->getTagsByName('@var')) { 63 | return $var[0]->value->type; 64 | } 65 | 66 | return null; 67 | } 68 | 69 | public function getReturnType(): ?TypeNode 70 | { 71 | if ($type = $this->newDocNode->getReturnTagValues()) { 72 | return $type[0]->type; 73 | } 74 | 75 | return null; 76 | } 77 | 78 | public function addTag(string $tagName, string $value): self 79 | { 80 | $this->newDocNode->children[] = new PhpDocTagNode($tagName, new GenericTagValueNode($value)); 81 | 82 | return $this; 83 | } 84 | 85 | public function removeTag(string $tagName): self 86 | { 87 | foreach ($this->newDocNode->children as $index => $tag) { 88 | if ($tag instanceof PhpDocTagNode && $tag->name === $tagName) { 89 | unset($this->newDocNode->children[$index]); 90 | } 91 | } 92 | 93 | return $this; 94 | } 95 | 96 | public function print(): string 97 | { 98 | $printer = new Printer(); 99 | $doc = $printer->printFormatPreserving($this->newDocNode, $this->docNode, $this->tokens); 100 | 101 | return '/** */' === $doc ? '' : $doc; 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /src/Util/Helper.php: -------------------------------------------------------------------------------- 1 | = 2) { 20 | $lastVar = $vars[count($vars) - 1]; 21 | if (is_array($lastVar) && array_intersect(array_keys($lastVar), self::$htmlConfig)) { 22 | $config = array_pop($vars); 23 | } 24 | } 25 | 26 | $start = memory_get_usage(); 27 | 28 | DoctrineProxy::init( 29 | $vars, 30 | $config['nestedLevel'] ?? $config['level'] ?? -1, 31 | $config['collectionSize'] ?? $config['size'] ?? 1 32 | ); 33 | 34 | $cloner = new VarCloner(); 35 | $cloner->setMaxString($config['maxString'] ?? -1); 36 | $cloner->setMaxItems(-1); 37 | $dumper = new HtmlDumper(); 38 | 39 | if (!is_dir($filePath = sys_get_temp_dir().'/psym/dump')) { 40 | mkdir($filePath, 0755, true); 41 | } 42 | 43 | $filePath = $filePath.'/'.time().'.html'; 44 | $output = fopen($filePath, 'w+'); 45 | $dumper->dump($cloner->cloneVar($vars), $output, [ 46 | 'maxStringLength' => $config['maxString'] ?? -1, 47 | 'maxDepth' => -1, 48 | ]); 49 | fclose($output); 50 | 51 | $end = memory_get_usage(); 52 | $memoryUsed = $end - $start; 53 | 54 | echo "\n Memory used: ".number_format($memoryUsed / (1024 * 1024), 2)." MB\n"; 55 | echo "\n \e]8;;file://$filePath\e\\\033[32m CLICK TO VIEW\033[0m\e]8;;\e\\\n\n"; 56 | } 57 | 58 | public static function partialSearch(array $subjects, string $searchTerm): array 59 | { 60 | $result = array_filter($subjects, function ($subject) use ($searchTerm) { 61 | return false !== stripos($subject, $searchTerm); 62 | }); 63 | 64 | $positions = []; 65 | foreach ($result as $subject) { 66 | $position = stripos($subject, $searchTerm); 67 | $positions[$subject] = $position; 68 | } 69 | 70 | // Sort subjects based on the position of the first match, in ascending order 71 | uasort($positions, function ($a, $b) { 72 | return $a <=> $b; 73 | }); 74 | 75 | return array_keys($positions); 76 | } 77 | 78 | public static function stringifyDefaultValue($defaultValue): string 79 | { 80 | if (is_array($defaultValue)) { 81 | if (array_keys($defaultValue) !== range(0, count($defaultValue) - 1)) { 82 | $default = ''; 83 | foreach ($defaultValue as $key => $value) { 84 | $default .= "'$key' => '$value', "; 85 | } 86 | $default = rtrim($default, ', '); 87 | } else { 88 | $default = implode(', ', $defaultValue); 89 | } 90 | $default = "[$default]"; 91 | } elseif (is_null($defaultValue)) { 92 | $default = 'null'; 93 | } elseif (is_bool($defaultValue)) { 94 | $default = ($defaultValue ? 'true' : 'false'); 95 | } elseif (is_string($defaultValue)) { 96 | $default = '"'.$defaultValue.'"'; 97 | } else { 98 | $default = var_export($defaultValue, true); 99 | } 100 | 101 | return $default; 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /src/Util/DoctrineProxy.php: -------------------------------------------------------------------------------- 1 | nestedLevel = $nestedLevel; 17 | $this->collectionSize = $collectionSize; 18 | $this->entities = DoctrineEM::getEntities(); 19 | } 20 | 21 | public static function init($vars, $nestedLevel, $collectionSize): void 22 | { 23 | $init = new self($nestedLevel, $collectionSize); 24 | $init->initialize($vars); 25 | } 26 | 27 | private function initialize($vars): void 28 | { 29 | if (is_array($vars)) { 30 | foreach ($vars as $var) { 31 | $this->initialize($var); 32 | } 33 | } 34 | 35 | if (is_object($vars) && $class = get_class($vars)) { 36 | $class = str_replace('Proxies\__CG__\\', '', $class); 37 | } 38 | 39 | if (isset($class) && in_array($class, $this->entities)) { 40 | $this->initializeEntity($vars, 0); 41 | } 42 | } 43 | 44 | private function initializeEntity($entity, $level): void 45 | { 46 | if (-1 !== $this->nestedLevel && $level > $this->nestedLevel) { 47 | return; 48 | } 49 | 50 | foreach ($this->getAllProperties($entity) as $value) { 51 | if ($value instanceof Proxy && !$value->__isInitialized()) { 52 | $value->__load(); 53 | $this->initializeEntity($value, $level + 1); 54 | } elseif ($value instanceof Collection && !$value->isInitialized()) { 55 | $this->setExtraLazy($value); 56 | $collection = $value->slice(0, -1 === $this->collectionSize ? null : $this->collectionSize); 57 | $value->setInitialized(true); 58 | 59 | if (!$collection) { 60 | continue; 61 | } 62 | 63 | foreach ($collection as $coll) { 64 | $this->initializeEntity($coll, $level + 1); 65 | $value->add($coll); 66 | } 67 | 68 | if (count($collection) === $this->collectionSize) { 69 | $value->add(sprintf('collection trimmed to %s items', $this->collectionSize)); 70 | } 71 | } 72 | } 73 | } 74 | 75 | private function setExtraLazy($collection): void 76 | { 77 | $reflection = new \ReflectionClass($collection); 78 | $property = $reflection->getProperty('association'); 79 | $property->setAccessible(true); 80 | 81 | $association = $property->getValue($collection); 82 | $association['fetch'] = 4; // FETCH_EXTRA_LAZY; 83 | 84 | $property->setValue($collection, $association); 85 | } 86 | 87 | private function getAllProperties($object): array 88 | { 89 | $properties = []; 90 | $reflectionClass = new \ReflectionClass($object); 91 | do { 92 | $currentProperties = $reflectionClass->getProperties(); 93 | foreach ($currentProperties as $property) { 94 | try { 95 | $property->setAccessible(true); 96 | $properties[$property->getName()] = $property->getValue($object); 97 | } catch (\Throwable $e) { 98 | // to avoid, typed property must not be accessed before initialization 99 | } 100 | } 101 | $reflectionClass = $reflectionClass->getParentClass(); 102 | } while ($reflectionClass); 103 | 104 | return $properties; 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /src/functions.php: -------------------------------------------------------------------------------- 1 | -1, 'size' or 'collectionSize' => 1, 'maxString' => -1] 16 | * Where: 17 | * - `nestedLevel` or `level` - how deep it should go to instantiate doctrine proxy object 18 | * - `collectionSize` or `size` - cut the Doctrine association collection to this specific size 19 | * - `maxString` - cut the overlong string to this specific size 20 | * 21 | * @return void 22 | */ 23 | function html(...$vars) 24 | { 25 | Helper::html(...$vars); 26 | } 27 | } 28 | 29 | if (!function_exists('table')) { 30 | /** 31 | * Retrieve a repository or query builder for a given entity. 32 | * 33 | * @param string $table the entity class name or table name 34 | * @param string|null $alias Optional. QueryBuilder alias. If provided, returns a QueryBuilder. 35 | * 36 | * @return EntityRepository|QueryBuilder|void returns EntityRepository if no alias is provided, 37 | * otherwise returns QueryBuilder 38 | */ 39 | function table(string $table, ?string $alias = null) 40 | { 41 | global $doctrine; 42 | 43 | if (!$doctrine) { 44 | dump('** No Doctrine found! **'); 45 | 46 | return; 47 | } 48 | 49 | $table = DoctrineEM::findEntityFullName($table) ?: $table; 50 | $repo = $doctrine->getRepository($table); 51 | 52 | return $alias ? $repo->createQueryBuilder($alias) : $repo; 53 | } 54 | } 55 | 56 | if (!function_exists('sql')) { 57 | /** 58 | * Executes a raw SQL query and returns the result. 59 | * 60 | * @param string $sql the SQL query string 61 | * @param array $params an associative array of query parameters 62 | * 63 | * @return array[]|void 64 | */ 65 | function sql(string $sql, array $params = []) 66 | { 67 | global $em; 68 | 69 | if (!$em) { 70 | dump('** No Doctrine found! **'); 71 | 72 | return; 73 | } 74 | 75 | try { 76 | $connection = $em->getConnection(); 77 | $stmt = $connection->prepare(trim($sql)); 78 | 79 | if (method_exists($stmt, 'executeQuery')) { 80 | $result = $stmt->executeQuery($params); 81 | $result = $result->fetchAllAssociative(); 82 | } else { 83 | $stmt->execute($params); 84 | $result = $stmt->fetchAll(FetchMode::ASSOCIATIVE); 85 | } 86 | 87 | return $result; 88 | } catch (\Doctrine\DBAL\Driver\Exception|\Doctrine\DBAL\Exception $e) { 89 | dump($e->getMessage()); 90 | } 91 | } 92 | } 93 | 94 | if (!function_exists('dql')) { 95 | /** 96 | * Executes a DQL (Doctrine Query Language) query and returns the result. 97 | * 98 | * @param string $dql the DQL query string 99 | * @param array $params an associative array of query parameters 100 | * 101 | * @return array[]|void 102 | */ 103 | function dql(string $dql, array $params = []) 104 | { 105 | global $em; 106 | 107 | if (!$em) { 108 | dump('** No Doctrine found! **'); 109 | 110 | return; 111 | } 112 | 113 | $query = $em->createQuery(trim($dql)); 114 | 115 | if ($params) { 116 | $query->setParameters($params); 117 | } 118 | 119 | return $query->getResult(AbstractQuery::HYDRATE_ARRAY); 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /src/Formatter/SignatureFormatter.php: -------------------------------------------------------------------------------- 1 | getName(); 30 | $parameters = self::getParametersSignature($function->getParameters()); 31 | 32 | // function foo(...) 33 | return sprintf( 34 | 'function %s(%s)', 35 | $name, 36 | implode(', ', $parameters) 37 | ); 38 | } 39 | 40 | public static function formatMethod(\ReflectionMethod $method): string 41 | { 42 | $modifiers = implode(' ', \Reflection::getModifierNames($method->getModifiers())); 43 | $name = $method->getName(); 44 | $parameters = self::getParametersSignature($method->getParameters()); 45 | 46 | // public static function foo(...) 47 | return sprintf( 48 | '%s function %s(%s)', 49 | $modifiers, 50 | $name, 51 | implode(', ', $parameters) 52 | ); 53 | } 54 | 55 | public static function formatProperty(\ReflectionProperty $property): string 56 | { 57 | $modifiers = implode(' ', \Reflection::getModifierNames($property->getModifiers())); 58 | $type = method_exists($property, 'hasType') && $property->hasType() ? self::formatReflectionType($property->getType()).' ' : ''; 59 | $name = $property->getName(); 60 | 61 | // private static ?int $count 62 | return sprintf( 63 | '%s %s$%s', 64 | $modifiers, 65 | $type, 66 | $name 67 | ); 68 | } 69 | 70 | private static function getParametersSignature(array $parameters): array 71 | { 72 | return array_map(function (\ReflectionParameter $param) { 73 | $type = $param->hasType() ? self::formatReflectionType($param->getType()).' ' : ''; 74 | $isNullable = $param->hasType() && $param->getType()->allowsNull() ? '?' : ''; 75 | $byReference = $param->isPassedByReference() ? '&' : ''; 76 | $isVariadic = $param->isVariadic() ? '...' : ''; 77 | $name = '$'.$param->getName(); 78 | $default = ''; 79 | 80 | if ($param->isDefaultValueAvailable()) { 81 | $defaultValue = $param->getDefaultValue(); 82 | $default = ' = '.Helper::stringifyDefaultValue($defaultValue); 83 | } 84 | 85 | // ?int &$number = 0, ...$params 86 | return sprintf( 87 | '%s%s%s%s%s%s', 88 | $isNullable, 89 | $type, 90 | $byReference, 91 | $isVariadic, 92 | $name, 93 | $default 94 | ); 95 | }, $parameters); 96 | } 97 | 98 | private static function formatReflectionType(\ReflectionType $type): string 99 | { 100 | if ($type instanceof \ReflectionNamedType) { 101 | return $type->getName(); 102 | } 103 | 104 | if ($type instanceof \ReflectionUnionType) { 105 | return implode('|', array_map(function (\ReflectionNamedType $t) { 106 | return $t->getName(); 107 | }, $type->getTypes())); 108 | } 109 | 110 | if ($type instanceof \ReflectionIntersectionType) { 111 | return implode('&', array_map(function (\ReflectionNamedType $t) { 112 | return $t->getName(); 113 | }, $type->getTypes())); 114 | } 115 | 116 | return (string) $type; 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /src/Command/ListEntitiesCommand.php: -------------------------------------------------------------------------------- 1 | setName('lse') 19 | ->setDescription('Lists all entities managed by Doctrine') 20 | ->addArgument('entityName', InputArgument::OPTIONAL, 'Entity name', '') 21 | ->addArgument('propertyName', InputArgument::OPTIONAL, 'Property name of the entity', '') 22 | ; 23 | } 24 | 25 | protected function execute(InputInterface $input, OutputInterface $output): int 26 | { 27 | $io = new SymfonyStyle($input, $output); 28 | $formatter = $output->getFormatter(); 29 | 30 | $entityName = $input->getArgument('entityName'); 31 | $propertyName = $input->getArgument('propertyName'); 32 | $entityFullName = DoctrineEM::findEntityFullName($entityName); 33 | 34 | $entities = DoctrineEM::getEntitiesTables(); 35 | $entityMapping = DoctrineEM::getEntitiesMappings()[$entityFullName] ?? []; 36 | $rows = []; 37 | 38 | if (!$entityFullName && $entityName) { 39 | $headers = ['entity', 'table']; 40 | $matches = $this->searchEntities($entities, $entityName); 41 | 42 | foreach ($matches as $entityMapping => $tableName) { 43 | $formattedEntity = $this->highlightMatches($entityMapping, $entityName, $formatter); 44 | $formattedTable = $this->highlightMatches($tableName, $entityName, $formatter); 45 | $rows[] = [$formattedEntity, $formattedTable]; 46 | } 47 | } elseif ($entityMapping && $propertyName) { 48 | $headers = ['property', 'column', 'type', 'default']; 49 | $matches = $this->searchProperties($entityMapping['properties'], $propertyName); 50 | 51 | foreach ($matches as $property => $map) { 52 | $formattedProperty = $this->highlightMatches($property, $propertyName, $formatter); 53 | $formattedColumn = $this->highlightMatches($map['column'], $propertyName, $formatter); 54 | $rows[] = [$formattedProperty, $formattedColumn, $map['type'], $map['default']]; 55 | } 56 | 57 | $io->text("$entityFullName:"); 58 | } elseif ($entityMapping) { 59 | $headers = ['property', 'column', 'type', 'default']; 60 | 61 | foreach ($entityMapping['properties'] as $property => $map) { 62 | $rows[] = [$property, $map['column'], $map['type'], $map['default']]; 63 | } 64 | 65 | $io->text("$entityFullName:"); 66 | } else { 67 | $headers = ['entity', 'table']; 68 | 69 | foreach ($entities as $entityName => $tableName) { 70 | $rows[] = [$entityName, $tableName]; 71 | } 72 | } 73 | 74 | $io->table($headers, $rows); 75 | 76 | return 0; 77 | } 78 | 79 | private function searchEntities(array $entities, string $searchTerm): array 80 | { 81 | return array_filter($entities, function ($tableName, $entityName) use ($searchTerm) { 82 | return false !== stripos($tableName, $searchTerm) || false !== stripos($entityName, $searchTerm); 83 | }, ARRAY_FILTER_USE_BOTH); 84 | } 85 | 86 | private function searchProperties(array $properties, string $searchTerm): array 87 | { 88 | return array_filter($properties, function ($map, $property) use ($searchTerm) { 89 | return false !== stripos($property, $searchTerm) || false !== stripos($map['column'], $searchTerm); 90 | }, ARRAY_FILTER_USE_BOTH); 91 | } 92 | 93 | private function highlightMatches(string $subject, string $searchTerm, OutputFormatterInterface $formatter): string 94 | { 95 | $searchTerm = preg_quote($searchTerm, '/'); 96 | 97 | if (preg_match("/($searchTerm)/i", $subject, $matches)) { 98 | $searchTerm = $matches[1]; 99 | } 100 | 101 | return str_ireplace($searchTerm, $formatter->format("$searchTerm"), $subject); 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /src/SFLoader.php: -------------------------------------------------------------------------------- 1 | projectDir = $projectDir; 21 | } 22 | 23 | public function getUsefulServices(): array 24 | { 25 | if ($this->usefulServices) { 26 | return $this->usefulServices; 27 | } 28 | 29 | $kernel = $this->getKernelInstance(); 30 | $container = $kernel ? $kernel->getContainer() : null; 31 | $doctrine = $container->has('doctrine') ? $container->get('doctrine') : null; 32 | $em = $doctrine ? $doctrine->getManager() : null; 33 | 34 | $GLOBALS['kernel'] = $kernel; 35 | $GLOBALS['container'] = $container; 36 | $GLOBALS['doctrine'] = $doctrine; 37 | $GLOBALS['em'] = $em; 38 | 39 | return $this->usefulServices = [$kernel, $container, $doctrine, $em]; 40 | } 41 | 42 | public function getAllCommands(): array 43 | { 44 | if (!$kernel = $this->getKernelInstance()) { 45 | return []; 46 | } 47 | 48 | $app = new Application($kernel); 49 | 50 | return $app->all(); 51 | } 52 | 53 | public function getKernelInstance() 54 | { 55 | $kernel = null; 56 | 57 | if ($this->kernelInstance) { 58 | return $this->kernelInstance; 59 | } 60 | 61 | try { 62 | if (!file_exists($console = $this->projectDir.'/bin/console')) { 63 | throw new \RuntimeException('Boot failed: bin/console file not found!'); 64 | } 65 | 66 | if (file_exists($this->projectDir.'/vendor/autoload_runtime.php') && $this->doesConsoleHaveRuntime($console)) { 67 | $runtime = new SymfonyRuntime(['project_dir' => $this->projectDir]); 68 | $app = require $console; 69 | [$app, $args] = $runtime->getResolver($app)->resolve(); 70 | $app = $app(['APP_ENV' => $args[0]['APP_ENV'] ?? 'dev', 'APP_DEBUG' => (bool) $args[0]['APP_DEBUG'] ?? true]); 71 | $kernel = $app->getKernel(); 72 | $kernel->boot(); 73 | 74 | return; 75 | } 76 | 77 | if (file_exists($bootstrap = $this->projectDir.'/config/bootstrap.php')) { 78 | require $bootstrap; 79 | } else { 80 | require __DIR__.'/bootstrap.php'; 81 | } 82 | 83 | if (!class_exists($kernelClass = $this->getKernelClass($console))) { 84 | if (!class_exists($kernelClass = 'App\Kernel')) { 85 | if (!class_exists($kernelClass = 'AppKernel')) { 86 | throw new \RuntimeException('Boot failed: kernel class not found!'); 87 | } 88 | } 89 | } 90 | 91 | $kernel = new $kernelClass($_SERVER['APP_ENV'] ?? 'dev', $_SERVER['APP_DEBUG'] ?? true); 92 | $kernel->boot(); 93 | } catch (\Throwable $error) { 94 | $this->displayWarning($error->getMessage()); 95 | } finally { 96 | if ($kernel && !$kernel->getContainer()->has('doctrine')) { 97 | $this->displayWarning('Doctrine not found'); 98 | } 99 | 100 | return $this->kernelInstance = $kernel; 101 | } 102 | } 103 | 104 | private function doesConsoleHaveRuntime(string $console): bool 105 | { 106 | $nodeVisitor = $this->getVisitedNodeVisitor($console); 107 | 108 | return $nodeVisitor->doesItReturnClosure; 109 | } 110 | 111 | private function getKernelClass(string $console): ?string 112 | { 113 | $nodeVisitor = $this->getVisitedNodeVisitor($console); 114 | 115 | return $nodeVisitor->kernelClass; 116 | } 117 | 118 | private function getVisitedNodeVisitor(string $file): NodeVisitor 119 | { 120 | if ($this->nodeVisitor) { 121 | return $this->nodeVisitor; 122 | } 123 | $code = file_get_contents($file); 124 | 125 | $parser = (new ParserFactory())->createForNewestSupportedVersion(); 126 | $ast = $parser->parse($code); 127 | $traverser = new NodeTraverser(); 128 | $nodeVisitor = new NodeVisitor(); 129 | $traverser->addVisitor($nodeVisitor); 130 | $traverser->traverse($ast); 131 | 132 | return $this->nodeVisitor = $nodeVisitor; 133 | } 134 | 135 | private function displayWarning(string $message): void 136 | { 137 | echo "\n\033[1;31m * $message\033[0m\n\n"; 138 | } 139 | } 140 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # pSym 2 | A REPL for Symfony and PHP 3 | 4 | **pSym** works both inside and outside Symfony project. When used within a Symfony project, it provides additional 5 | features such as access to variables like `$kernel`, `$container`, `$doctrine`, and `$em`. 6 | Additionally, all registered **project commands** become accessible as well. 7 | 8 | The `lse` command and `table()`, `sql()`, and `dql()` functions are available when **Doctrine** is installed. 9 | 10 | Function `html()` and features like `auto-completion`, `auto-suggestion`, and `doc-and-signature` work **universally**. 11 | 12 | ## Installation 13 | To install it, create a project using `composer create-project` in your preferred location. It supports 14 | PHP versions `>=7.2` and works with Symfony versions `4`, `5`, `6`, and `7`. 15 | ```shell 16 | # The home directory might be an ideal place to install 17 | cd ~ 18 | composer create-project tareqas/psym psym 19 | ``` 20 | Next, add the `your-psym-installation-path/psym/bin/` directory to your system's PATH, so you can run the 21 | `psym` command directly from your shell. 22 | 23 | > **Warning:** Do not install it as a global or local package; it won't work. 24 | 25 | ### Commands 26 | ```shell 27 | # list all the commands, including your project commands. 28 | list 29 | # or 30 | ? 31 | ``` 32 | 33 | ### Auto-complete and Auto-suggestion 34 | ![auto-completion](https://raw.githubusercontent.com/tareqas/psym/d4a4b9064035e4eb36e95e6a09a6e0de3c22ba9c/docs/images/auto-completion.gif) 35 | 36 | To get suggestions, press the `TAB` key. 37 | 38 | > **Note:** Sometimes you may need to press `SPACE` first and then `TAB`. 39 | 40 | ```shell 41 | # press TAB for suggestion 42 | $kernel-> 43 | # it also works with method chaining 44 | $kernel->getBundle()-> 45 | # press TAB for completion 46 | $kernel->getBund 47 | ``` 48 | 49 | ### Documentation and Signature 50 | ![documentation and signature](https://raw.githubusercontent.com/tareqas/psym/d4a4b9064035e4eb36e95e6a09a6e0de3c22ba9c/docs/images/doc-and-html.gif) 51 | 52 | You can view PHPDoc documentation and signature for `function`, `property`, and `method`. 53 | ```shell 54 | # press TAB to display the phpDoc and signature for getBundle 55 | $kernel->getBundle 56 | ``` 57 | 58 | ### lse 59 | ![lse](https://raw.githubusercontent.com/tareqas/psym/d4a4b9064035e4eb36e95e6a09a6e0de3c22ba9c/docs/images/lse.gif) 60 | 61 | The `lse` command lists all entities managed by Doctrine. 62 | ```shell 63 | # list of of all matching tables 64 | lse ca 65 | # list all properties, columns, types, and default values of an entity 66 | lse cart 67 | # list of all matching properties for the 'cart' entity 68 | lse cart tot 69 | ``` 70 | 71 | ### html() 72 | ```php 73 | function html(...$vars): void 74 | ``` 75 | The `html()` function dumps variables and renders them as a browsable HTML page. If any of your variables contain 76 | Doctrine objects, it will automatically instantiate all proxy objects. 77 | 78 | You can fine-tune the dump by providing additional options in the last parameter as an associative array: 79 | ```php 80 | html($var, [ 81 | 'nestedLevel' => -1, # or 'level' - how deep it should go to instantiate doctrine proxy object 82 | 'collectionSize' => 1, # or 'size' - cut the Doctrine association collection to this specific size 83 | 'maxString' => -1 # cut the overlong string to this specific size 84 | ]) 85 | # -1 implies no limit. 86 | ``` 87 | 88 | ### table() 89 | ```php 90 | function table(string $table, ?string $alias = null): EntityRepository|QueryBuilder|void 91 | ``` 92 | The `table()` function retrieves a repository for a given entity. It returns a `Doctrine\ORM\EntityRepository` 93 | if no alias is provided, or a `Doctrine\ORM\QueryBuilder` if an alias is specified. 94 | 95 | ### sql() 96 | ![sql](https://raw.githubusercontent.com/tareqas/psym/d4a4b9064035e4eb36e95e6a09a6e0de3c22ba9c/docs/images/sql.gif) 97 | 98 | ```php 99 | function sql(string $sql, array $params = []): array|void 100 | ``` 101 | The `sql()` function executes raw SQL queries and returns the result as an associative array. 102 | Doctrine is required to use this feature. 103 | ```shell 104 | # press TAB to display all available tables 105 | sql('select * from ' 106 | # press TAB to display all available columns in the 'cart' table 107 | sql('select c. from cart c' 108 | ``` 109 | 110 | ### dql() 111 | ![dql](https://raw.githubusercontent.com/tareqas/psym/d4a4b9064035e4eb36e95e6a09a6e0de3c22ba9c/docs/images/dql.gif) 112 | 113 | ```php 114 | function dql(string $dql, array $params = []): array|void 115 | ``` 116 | The `dql()` function allows you to execute DQL queries and also returns the result as an associative array. 117 | ```shell 118 | # press TAB to display all available entities 119 | dql('select * from ' 120 | # press TAB to display all available properties in the 'Cart' entity 121 | sql('select c. from App\Entity\Cart c' 122 | ``` 123 | > **Limitation:** Auto-completion may have limitations with entity classes due to backslashes (\\). 124 | > Other features work as expected. 125 | 126 | ## And more 127 | To unlock the full potential, explore the [PsySH documentation](https://psysh.org/#docs). pSym is built on top of PsySH. 128 | -------------------------------------------------------------------------------- /src/Parser/SqlParser.php: -------------------------------------------------------------------------------- 1 | type = $type; 33 | $this->rawSql = $rawSql; 34 | } 35 | 36 | public function getTables(): array 37 | { 38 | if ($this->tables) { 39 | return $this->tables; 40 | } 41 | 42 | $sql = $this->sanitizeSql($this->rawSql); 43 | $pattern = '/\b(?:FROM|JOIN|UPDATE) ([\w\[\]\\\."\'`]+)(?: AS)?( [\w\[\]\\\."\'`]+)?/i'; 44 | 45 | preg_match_all($pattern, trim($sql), $matches, PREG_SET_ORDER); 46 | 47 | foreach ($matches as $match) { 48 | $table = $this->trimQuotes($match[1]); 49 | $alias = $this->trimQuotes($match[2] ?? ''); 50 | 51 | if (false !== stripos($match[0], 'JOIN') && false !== stripos($table, '.')) { 52 | if ('dql' !== $this->type) { 53 | continue; 54 | } 55 | 56 | [$sourceAlias, $sourceColumn] = explode('.', $table); 57 | if (!$sourceTable = $this->findByTableOrAlias($sourceAlias)) { 58 | continue; 59 | } 60 | 61 | $sourceTable = DoctrineEM::findEntityFullName($sourceTable['table']); 62 | if (!$mapping = DoctrineEM::getEntitiesMappings()[$sourceTable]) { 63 | continue; 64 | } 65 | 66 | if (!$targetEntity = $mapping['properties'][$sourceColumn]['targetEntity'] ?? '') { 67 | continue; 68 | } 69 | 70 | $table = $targetEntity; 71 | } elseif ('dql' === $this->type && !$table = DoctrineEM::findEntityFullName($table)) { 72 | continue; 73 | } 74 | 75 | $this->tables[] = [ 76 | 'table' => $table, 77 | 'alias' => $alias, 78 | ]; 79 | } 80 | 81 | return $this->tables; 82 | } 83 | 84 | public function askingFor(array $info): array 85 | { 86 | $substring = substr($info['line_buffer'], 0, $info['point']); 87 | $substring = preg_replace('/^(sql|dql)\(/i', '', $substring); 88 | $substring = trim($this->removeExtraSpaces($substring), '"\'`'); 89 | $extracted = ['type' => '', 'name' => '', 'alias' => '']; 90 | 91 | if (preg_match('/(\w+)\.(\w+)?$/i', $substring, $match)) { 92 | $extracted['type'] = 'COLUMN'; 93 | $extracted['name'] = $this->trimQuotes($match[2] ?? ''); 94 | $extracted['alias'] = $this->trimQuotes($match[1] ?? ''); 95 | } elseif (preg_match('/\b(?:FROM|JOIN|UPDATE) ([\w\[\]\\\."\'`]+)?$/i', $substring, $match)) { 96 | $extracted['type'] = 'TABLE'; 97 | $extracted['name'] = $this->trimQuotes($match[1] ?? ''); 98 | $extracted['alias'] = $this->trimQuotes($match[2] ?? ''); 99 | } elseif (preg_match('/(?:\b(\w+)| )$/i', $substring, $match)) { 100 | $extracted['type'] = 'KEYWORDS'; 101 | $extracted['name'] = $this->trimQuotes($match[1] ?? ''); 102 | } elseif ('' === $substring) { 103 | $extracted['type'] = 'KEYWORDS_STARTING'; 104 | } 105 | 106 | return $extracted; 107 | } 108 | 109 | public function findByTableOrAlias(string $tableOrAlias): array 110 | { 111 | if (!$tableOrAlias) { 112 | return []; 113 | } 114 | 115 | $result = array_filter($this->getTables(), function ($tab) use ($tableOrAlias) { 116 | return $tab['table'] === $tableOrAlias || $tab['alias'] === $tableOrAlias; 117 | }); 118 | 119 | return reset($result) ?: []; 120 | } 121 | 122 | private function sanitizeSql(string $sql): string 123 | { 124 | $sql = preg_replace('/\s+/', ' ', strtolower($sql)); 125 | 126 | return $this->trimQuotes($this->removeExtraSpaces($sql)); 127 | } 128 | 129 | private function removeExtraSpaces(string $text): string 130 | { 131 | return preg_replace('/\s+/', ' ', $text); 132 | } 133 | 134 | private function trimQuotes(string $text): string 135 | { 136 | return trim($text, " \t\n\r\0\x0B'\"`[]"); 137 | } 138 | } 139 | -------------------------------------------------------------------------------- /src/Util/DoctrineEM.php: -------------------------------------------------------------------------------- 1 | 'ONE_TO_ONE', 11 | '2' => 'MANY_TO_ONE', 12 | '4' => 'ONE_TO_MANY', 13 | '8' => 'MANY_TO_MANY', 14 | ]; 15 | 16 | public static function findEntityFullName($entityOrTable): string 17 | { 18 | $entityOrTable = array_reverse(explode('\\', $entityOrTable))[0]; 19 | 20 | foreach (self::getEntitiesTables() as $entityName => $tableName) { 21 | $entityClassName = array_reverse(explode('\\', $entityName))[0]; 22 | if ( 23 | strtolower($tableName) === strtolower($entityOrTable) || 24 | strtolower($entityClassName) === strtolower($entityOrTable) 25 | ) { 26 | return $entityName; 27 | } 28 | } 29 | 30 | return ''; 31 | } 32 | 33 | public static function getEntitiesTables(): array 34 | { 35 | $entitiesTables = []; 36 | 37 | foreach (self::getAllMetadata() as $classMetadata) { 38 | $entitiesTables[$classMetadata->name] = $classMetadata->table['name']; 39 | } 40 | 41 | return $entitiesTables; 42 | } 43 | 44 | public static function getEntities(): array 45 | { 46 | return array_keys(self::getEntitiesTables()); 47 | } 48 | 49 | public static function getTables(): array 50 | { 51 | return array_values(self::getEntitiesTables()); 52 | } 53 | 54 | public static function getPropertiesColumns($entityName): array 55 | { 56 | global $em; 57 | $properties = []; 58 | $entityName = self::findEntityFullName($entityName); 59 | $metadata = $em->getMetadataFactory()->getAllMetadata(); 60 | 61 | foreach ($metadata as $classMetadata) { 62 | if ($classMetadata->getName() !== $entityName) { 63 | continue; 64 | } 65 | $properties = $classMetadata->columnNames; 66 | break; 67 | } 68 | 69 | return $properties; 70 | } 71 | 72 | public static function getProperties($entityName): array 73 | { 74 | return array_keys(self::getPropertiesColumns($entityName)); 75 | } 76 | 77 | public static function getColumns($entityName): array 78 | { 79 | return array_values(self::getPropertiesColumns($entityName)); 80 | } 81 | 82 | public static function getEntitiesMappings(): array 83 | { 84 | $entities = []; 85 | 86 | foreach (self::getAllMetadata() as $classMetadata) { 87 | $className = $classMetadata->getName(); 88 | $info = [ 89 | 'table' => $classMetadata->getTableName(), 90 | 'properties' => [], 91 | ]; 92 | 93 | foreach ($classMetadata->fieldMappings as $property => $mapping) { 94 | $type = $mapping['type']; 95 | $type = $mapping['nullable'] ?? false ? '?'.$type : $type; 96 | $type = $mapping['unique'] ?? false ? $type.'*' : $type; 97 | $type = $mapping['id'] ?? false ? $type.' [id]' : $type; 98 | $type = $mapping['length'] ?? false ? $type." ({$mapping['length']})" : $type; 99 | 100 | $info['properties'][$property] = [ 101 | 'column' => $mapping['columnName'], 102 | 'type' => $type, 103 | 'targetEntity' => '', 104 | ]; 105 | } 106 | 107 | foreach ($classMetadata->associationMappings as $property => $mapping) { 108 | $columnNames = isset($mapping['joinColumns']) ? array_map(function ($map) { 109 | return $map['name']; 110 | }, $mapping['joinColumns']) : []; 111 | $mappedBy = isset($mapping['mappedBy']) ? "::\${$mapping['mappedBy']}" : ''; 112 | $type = (self::$associations[$mapping['type']] ?? '')." ({$mapping['targetEntity']}$mappedBy)"; 113 | 114 | $info['properties'][$property] = [ 115 | 'column' => join(', ', $columnNames), 116 | 'type' => $type, 117 | 'targetEntity' => $mapping['targetEntity'], 118 | ]; 119 | } 120 | 121 | $properties = []; 122 | foreach (self::getAllProperties($className) as $name => $value) { 123 | if (isset($info['properties'][$name])) { 124 | $propInfo = $info['properties'][$name]; 125 | $propInfo['default'] = $value; 126 | $properties[$name] = $propInfo; 127 | } 128 | } 129 | $info['properties'] = $properties; 130 | 131 | $entities[$className] = $info; 132 | } 133 | 134 | return $entities; 135 | } 136 | 137 | private static function getAllMetadata(): array 138 | { 139 | /* @var EntityManagerInterface $em */ 140 | global $em; 141 | 142 | if (!$em) { 143 | return []; 144 | } 145 | 146 | return $em->getMetadataFactory()->getAllMetadata(); 147 | } 148 | 149 | private static function getAllProperties($class): array 150 | { 151 | $properties = []; 152 | $reflection = new \ReflectionClass($class); 153 | do { 154 | $currentProperties = $reflection->getProperties(); 155 | $defaultProperties = $reflection->getDefaultProperties(); 156 | 157 | foreach ($currentProperties as $property) { 158 | $propertyName = $property->getName(); 159 | $value = null; // null represents lack of value or default value 160 | if (array_key_exists($propertyName, $defaultProperties)) { 161 | $value = Helper::stringifyDefaultValue($defaultProperties[$propertyName]); 162 | } 163 | $properties[$propertyName] = $value; 164 | } 165 | $reflection = $reflection->getParentClass(); 166 | } while ($reflection); 167 | 168 | return $properties; 169 | } 170 | } 171 | -------------------------------------------------------------------------------- /tests/TabCompletion/AutoCompleterTest.php: -------------------------------------------------------------------------------- 1 | setContext($context); 42 | } 43 | $tabCompletion->addMatcher($matcher); 44 | } 45 | 46 | $context->setAll(['foo' => new Foo()]); 47 | 48 | $mock = Mockery::mock('alias:'.DoctrineEM::class); 49 | $mock->shouldReceive('getTables') 50 | ->andReturn(['table', 'table_2', 'table_3']); 51 | $mock->shouldReceive('getColumns') 52 | ->andReturn(['col', 'col_2', 'col_3']); 53 | $mock->shouldReceive('getEntities') 54 | ->andReturn(['App\Entity\Table', 'App\Entity\Table2', 'App\Entity\Table3']); 55 | $mock->shouldReceive('getProperties') 56 | ->andReturn(['prop', 'prop2', 'prop3']); 57 | 58 | $code = $tabCompletion->processCallback('', 0, [ 59 | 'line_buffer' => $line, 60 | 'point' => $point, 61 | 'end' => \strlen($line), 62 | ]); 63 | 64 | self::assertSame($expected, $code); 65 | } 66 | 67 | private function classesInput(): array 68 | { 69 | return [ 70 | /* MethodChainingMatcher */ 71 | ['$tmp = foo', ['foo()', 'tareqas\psym\tests\fixtures\tabcompletion\funcfoo()']], 72 | 73 | ['foo ', [$this->getDocAndSignature('foo'), '']], 74 | ['foo(', [$this->getDocAndSignature('foo'), '']], 75 | ['foo($bar', ['bar']], // add extra space at the end 'foo($bar ' 76 | ['foo($bar, baz()->', [$this->getDocAndSignature('foo'), '']], 77 | // a problem with displaying on the console. 78 | ['foo(baz()', [$this->getDocAndSignature('foo'), '']], 79 | ['foo(baz() ', [$this->getDocAndSignature('foo'), '']], 80 | 81 | ['foo()->union', ['union()', 'unionDoc()']], 82 | ['foo()->bar', ['bar()', '_bar']], 83 | ['foo()->ba', ['bar()', 'baz()', '_bar', '_baz']], 84 | 85 | ['foo()->noReturn() ', [$this->getDocAndSignature('noReturn'), '']], 86 | ['foo()->doesNotExist->', ["\n ** unknown: foo()->doesNotExist->\n", '']], 87 | 88 | ['foo()->bar()->', ['foo()', 'fooBar()', 'fooDocBar()']], 89 | ['foo()->bar->', ["\n ** Invalid method closing: foo()->bar->\n", '']], 90 | 91 | ['foo', ['foo()', 'tareqas\psym\tests\fixtures\tabcompletion\funcfoo()']], 92 | ['funcfoo', ['tareqas\psym\tests\fixtures\tabcompletion\funcfoo()']], 93 | ['min() ', [$this->getDocAndSignature('min'), '']], 94 | ['\TareqAS\Psym\Tests\Fixtures\TabCompletion\funcFoo()->_bar->', ['foo()', 'fooBar()', 'fooDocBar()']], 95 | ['TareqAS\Psym\Tests\Fixtures\TabCompletion\funcFoo()->_baz->', ['foo()', 'fooBaz()', 'fooDocBaz()']], 96 | ['\TareqAS\Psym\Tests\Fixtures\TabCompletion\Foo::init()->_bar->foo()->_baz->', ['foo()', 'fooBaz()', 'fooDocBaz()']], 97 | ['TareqAS\Psym\Tests\Fixtures\TabCompletion\Foo::init()->_bar->foo()->_baz->', ['foo()', 'fooBaz()', 'fooDocBaz()']], 98 | ['$foo->bar()->', ['foo()', 'fooBar()', 'fooDocBar()']], 99 | ['$foo->_baz->', ['foo()', 'fooBaz()', 'fooDocBaz()']], 100 | ['$tmp = $foo->_bar->', ['foo()', 'fooBar()', 'fooDocBar()']], 101 | ['$foo->bar(\'hi\', $foo->bar(), foo(\'there\'))->', ['foo()', 'fooBar()', 'fooDocBar()']], 102 | ['$foo->union()->', ['foo()', 'fooBar()', 'fooDocBar()', 'fooBaz()', 'fooDocBaz()']], 103 | ['$foo->unionDoc()->', ['foo()', 'fooBar()', 'fooDocBar()', 'fooBaz()', 'fooDocBaz()']], 104 | ['$foo->intersection()->', ['foo()']], 105 | ['$foo->intersectionDoc()->', ['foo()']], 106 | ['$foo->bar()->foo() ', [$this->getDocAndSignature('inheritdoc'), '']], 107 | 108 | /* SqlDqlMatcher */ 109 | ['sql()', [$this->getDocAndSignature('sql'), '']], 110 | ['sql(', SqlParser::$keywordsStarting], 111 | ['sql(`se', ['select', 'set', 'as', 'asc', 'is', 'case', 'desc', 'distinct', 'insert', 'constraint', 'exists', 'values', 'database'], strlen('sql(`e')], 112 | ['sql(\'selec', ['select'], strlen('sql(\'selec')], 113 | ['sql("selec', ['select'], strlen('sql("selec')], 114 | ['sql(`selec', ['select'], strlen('sql(`selec')], 115 | ['sql("select * from ', ['table', 'table_2', 'table_3'], strlen('sql("select * from ')], 116 | ['sql("select * from tab', ['table', 'table_2', 'table_3'], strlen('sql("select * from tab')], 117 | ['sql("select * from table', ['table', 'table_2', 'table_3'], strlen('sql("select * from table')], 118 | ['sql("select t1. from table t1', ['t1.col', 't1.col_2', 't1.col_3'], strlen('sql("select t1.')], 119 | ['sql("select t1.col from table t1', ['t1.col', 't1.col_2', 't1.col_3'], strlen('sql("select t1.col')], 120 | ['sql("select t1.col from table t1 jo', ['join'], strlen('sql("select t1.col from table t1 jo')], 121 | ['sql("select t1.col from table t1 join ', ['table', 'table_2', 'table_3'], strlen('sql("select t1.col from table t1 join ')], 122 | ['sql("select t1. from table t1 join table_2 t2 on t2. = t1.col', ['t1.col', 't1.col_2', 't1.col_3'], strlen('sql("select t1.')], 123 | ['sql("select t1. from table t1 join table_2 t2 on t2. = t1.col', ['t2.col', 't2.col_2', 't2.col_3'], strlen('sql("select t1. from table t1 join table_2 t2 on t2.')], 124 | ['sql("select t1.col from table t1', SqlParser::$keywords, strlen('sql("select t1.col ')], 125 | ['sql("select t1.col from table t1', SqlParser::$keywords, strlen('sql("select t1.col from table ')], 126 | ['sql("select t1.col from table t1 ', SqlParser::$keywords, strlen('sql("select t1.col from table t1 ')], 127 | ['sql("select *, (select t2.col_2 from table_2 t2 limit 1) from table")', ['t2.col', 't2.col_2', 't2.col_3'], strlen('sql("select *, (select t2.')], 128 | ['sql("invalid sintex', ['sintex'], strlen('sql("invalid sintex')], 129 | ['dql("select from ', ['App\Entity\Table', 'App\Entity\Table2', 'App\Entity\Table3'], strlen('dql("select from ')], 130 | ]; 131 | } 132 | 133 | private function getDocAndSignature($name): string 134 | { 135 | $docs = [ 136 | 'foo' => <<<'DOC' 137 | /** 138 | * A global user-defined function for testing 139 | * 140 | * @return Foo 141 | */ 142 | function foo() 143 | 144 | DOC, 145 | 'inheritdoc' => <<<'DOC' 146 | /** 147 | * You're following inheritdoc to get me 148 | * 149 | * @return Foo 150 | */ 151 | public function foo() 152 | 153 | DOC, 154 | 'min' => <<<'DOC' 155 | /** 156 | * Find lowest value 157 | * @link https://php.net/manual/en/function.min.php 158 | * @param array|mixed $value Array to look through or first value to compare 159 | * @param mixed ...$values any comparable value 160 | * @return mixed min returns the numerically lowest of the 161 | * parameter values. 162 | */ 163 | function min(?mixed $value, ?mixed ...$values) 164 | 165 | DOC, 166 | 'noReturn' => <<<'DOC' 167 | /** 168 | * it has no return type 169 | */ 170 | public function noReturn() 171 | 172 | DOC, 173 | 'sql' => <<<'DOC' 174 | /** 175 | * Executes a raw SQL query and returns the result. 176 | * 177 | * @param string $sql the SQL query string 178 | * @param array $params an associative array of query parameters 179 | * 180 | * @return array[]|void 181 | */ 182 | function sql(string $sql, array $params = []) 183 | 184 | DOC 185 | ]; 186 | 187 | return $docs[$name]; 188 | } 189 | } 190 | -------------------------------------------------------------------------------- /src/TabCompletion/Matcher/MethodChainingMatcher.php: -------------------------------------------------------------------------------- 1 | tokens = $this->preprocessTokens($tokens); 20 | $this->supportTokens = [T_VARIABLE, T_STRING, T_NAME_QUALIFIED, T_NAME_FULLY_QUALIFIED, X_STATIC_METHOD]; 21 | 22 | $parentheses = []; 23 | $nextTokenName = null; 24 | $prevTokenName = null; 25 | 26 | foreach ($this->tokens as $index => $token) { 27 | if ($this->ignoreVariableAssignment($index)) { 28 | continue; 29 | } 30 | 31 | if ($prevTokenName && $this->isWithinParameterParentheses($token, $parentheses)) { 32 | if (in_array($prevTokenName, $this->supportTokens) && $this->isLast($index)) { 33 | return true; 34 | } 35 | continue; 36 | } 37 | 38 | // [supportTokens] (-> T_STRING (->)?)* 39 | if (!$nextTokenName && in_array($token[0], $this->supportTokens)) { 40 | $nextTokenName = T_OBJECT_OPERATOR; 41 | $prevTokenName = $token[0]; 42 | } elseif ($token[0] === $nextTokenName) { 43 | $nextTokenName = T_STRING === $nextTokenName ? T_OBJECT_OPERATOR : T_STRING; 44 | $prevTokenName = $token[0]; 45 | } else { 46 | return false; 47 | } 48 | 49 | if ($nextTokenName && $this->isLast($index)) { 50 | return true; 51 | } 52 | } 53 | 54 | return false; 55 | } 56 | 57 | public function getMatches(array $tokens, array $info = []): array 58 | { 59 | array_shift($tokens); 60 | $this->tokens = $this->preprocessTokens($tokens); 61 | 62 | $input = $info['line_buffer']; 63 | $hasEndingSpace = ' ' === substr($info['line_buffer'], -1); 64 | 65 | $type = ''; 66 | $identifier = ''; 67 | $stack = []; 68 | $suggestions = []; 69 | $parentheses = []; 70 | 71 | foreach ($this->tokens as $index => $token) { 72 | if ($this->ignoreVariableAssignment($index)) { 73 | continue; 74 | } 75 | 76 | if ($this->isWithinParameterParentheses($token, $parentheses)) { 77 | // asking function or method signature while writing params, foo(.... 78 | if ($stack && $this->isLast($index)) { 79 | // Adding ")" to this list disrupts other matchers. 80 | if (preg_match('/\b(\w+)\b$/', $input, $matches)) { 81 | return [$matches[1]]; 82 | } 83 | ['token' => $token, 'type' => $type] = array_pop($stack); 84 | $suggestions = $this->getSuggestions($token, $type, true); 85 | } 86 | continue; 87 | } 88 | 89 | if (in_array($token[0], $this->supportTokens)) { 90 | if ($this->isLast($index)) { 91 | $suggestions = $this->getSuggestions($token, $type, $hasEndingSpace); 92 | break; 93 | } 94 | 95 | $next = $this->getNextType($token, $type); 96 | if (!$next['type'] && $next['unknown']) { 97 | $suggestions = $this->getMessageWithInvalidPosition($index, 'unknown'); 98 | break; 99 | } 100 | 101 | $stack[] = compact('token', 'type', 'identifier'); 102 | $type = $next['type']; 103 | $identifier = $next['identifier']; 104 | } elseif (T_OBJECT_OPERATOR === $token[0]) { 105 | if (!$this->isMethodClosedProperly($identifier, $index)) { 106 | $suggestions = $this->getMessageWithInvalidPosition(max($index - 1, 0), 'Invalid method closing'); 107 | break; 108 | } 109 | $suggestions = $this->getSuggestions($token, $type); 110 | } else { 111 | break; 112 | } 113 | } 114 | 115 | return $suggestions; 116 | } 117 | 118 | private function ignoreVariableAssignment($currentIndex): bool 119 | { 120 | if (count($this->tokens) >= 2 && $currentIndex < 2 && T_VARIABLE === $this->tokens[0][0] && '=' === $this->tokens[1]) { 121 | return true; 122 | } 123 | 124 | return false; 125 | } 126 | 127 | private function isWithinParameterParentheses($token, &$parentheses): bool 128 | { 129 | if ('(' === $token) { 130 | $parentheses[] = $token; 131 | } elseif (')' === $token) { 132 | array_pop($parentheses); 133 | } 134 | 135 | return !empty($parentheses) || ')' === $token; 136 | } 137 | 138 | private function isLast($currentIndex): bool 139 | { 140 | return !isset($this->tokens[$currentIndex + 1]); 141 | } 142 | 143 | private function isMethodClosedProperly($identifier, $currentIndex): bool 144 | { 145 | if ('method' !== $identifier || !isset($this->tokens[$currentIndex - 1])) { 146 | return true; 147 | } 148 | 149 | return ')' === $this->tokens[$currentIndex - 1]; 150 | } 151 | 152 | private function getMessageWithInvalidPosition($position, $message): array 153 | { 154 | $input = ''; 155 | foreach ($this->tokens as $index => $token) { 156 | $token = is_array($token) ? $token[1] : $token; 157 | if ($index === $position) { 158 | $input .= "$token"; 159 | } else { 160 | $input .= $token; 161 | } 162 | } 163 | 164 | return Paint::message(" ** $message: $input"); 165 | } 166 | 167 | private function getNextType(array $token, string $type = ''): array 168 | { 169 | $info = ['type' => '', 'identifier' => '', 'unknown' => false]; 170 | $name = $token[1]; 171 | 172 | if (T_VARIABLE === $token[0]) { 173 | try { 174 | $info['identifier'] = 'prop'; 175 | $var = str_replace('$', '', $name); 176 | $object = $this->getVariable($var); 177 | if (is_object($object)) { 178 | $info['type'] = get_class($object); 179 | } 180 | } catch (\Exception $e) { 181 | $info['unknown'] = true; 182 | } 183 | 184 | return $info; 185 | } 186 | 187 | if (!in_array($token[0], $this->supportTokens)) { 188 | $info['unknown'] = true; 189 | 190 | return $info; 191 | } 192 | 193 | if (X_STATIC_METHOD === $token[0]) { 194 | [$type, $name] = explode('::', $token[1]); 195 | } 196 | 197 | // for function, it cannot have type. Otherwise, foo from A\B::foo() will check global function foo() 198 | if (!$type && function_exists($token[1])) { 199 | $function = Reflection::getFunction($token[1]); 200 | $info['identifier'] = 'method'; 201 | $info['type'] = $function['type']; 202 | 203 | return $info; 204 | } 205 | 206 | if ($class = Reflection::getClassFromType($type, $name)) { 207 | $info['identifier'] = $class['identifier']; 208 | $info['type'] = $class['type']; 209 | } else { 210 | $info['unknown'] = true; 211 | } 212 | 213 | return $info; 214 | } 215 | 216 | private function getSuggestions(array $token, ?string $type = null, bool $showDetails = false): array 217 | { 218 | $name = T_OBJECT_OPERATOR === $token[0] ? null : $token[1]; 219 | 220 | if (X_STATIC_METHOD === $token[0]) { 221 | [$type, $name] = explode('::', $token[1]); 222 | } 223 | 224 | if ($type && $suggestions = $this->getSuggestionsForClass($type, $name, $showDetails)) { 225 | return $suggestions; 226 | } 227 | 228 | // for function, it cannot have type. Otherwise, foo from A\B::foo() will check global function foo() 229 | if (!$type && $suggestions = $this->getSuggestionsForFunction($name, $showDetails)) { 230 | return $suggestions; 231 | } 232 | 233 | return []; 234 | } 235 | 236 | private function getSuggestionsForFunction($functionName, $showDetails = false): array 237 | { 238 | $functions = get_defined_functions(); 239 | $functions = array_merge($functions['internal'], $functions['user']); 240 | 241 | if (!$functions) { 242 | return []; 243 | } 244 | 245 | $functions = Helper::partialSearch($functions, $functionName); 246 | $suggestions = array_map(function ($function) { return "$function()"; }, $functions); 247 | 248 | if ($showDetails && $function = Reflection::getFunction($functionName)) { 249 | $suggestions = Paint::docAndSignature($function['doc'], $function['signature']); 250 | } 251 | 252 | return $suggestions; 253 | } 254 | 255 | private function getSuggestionsForClass($type, $propOrMethodName = null, $showDetails = false): array 256 | { 257 | if (!$class = Reflection::getClassFromType($type)) { 258 | return []; 259 | } 260 | 261 | $methods = array_map(function ($class) { 262 | return $class['name']; 263 | }, array_merge($class['staticMethods'], $class['methods'])); 264 | 265 | $properties = array_map(function ($property) { 266 | return $property['name']; 267 | }, array_merge($class['staticProperties'], $class['properties'])); 268 | 269 | if (!$propOrMethodName) { 270 | $methods = array_map(function ($method) { return "$method()"; }, $methods); 271 | 272 | return array_merge($methods, $properties); 273 | } 274 | 275 | $methods = Helper::partialSearch($methods, $propOrMethodName); 276 | $methods = array_map(function ($method) { return "$method()"; }, $methods); 277 | $properties = Helper::partialSearch($properties, $propOrMethodName); 278 | $suggestions = array_merge($methods, $properties); 279 | 280 | if ($showDetails && $item = Reflection::getClassItemFromType($type, $propOrMethodName)) { 281 | $suggestions = Paint::docAndSignature($item['doc'], $item['signature']); 282 | } 283 | 284 | return $suggestions; 285 | } 286 | 287 | private function preprocessTokens(array $tokens): array 288 | { 289 | if (!defined('T_NAME_FULLY_QUALIFIED')) { 290 | define('T_NAME_FULLY_QUALIFIED', 10001); 291 | } 292 | if (!defined('T_NAME_QUALIFIED')) { 293 | define('T_NAME_QUALIFIED', 10002); 294 | } 295 | if (!defined('X_STATIC_METHOD')) { 296 | define('X_STATIC_METHOD', 10003); 297 | } 298 | 299 | $i = 0; 300 | $count = count($tokens); 301 | $newTokens = []; 302 | 303 | while ($i < $count) { 304 | $token = $tokens[$i]; 305 | 306 | if (is_array($token) && T_NS_SEPARATOR === $token[0]) { 307 | ++$i; 308 | $name = ''; 309 | 310 | while ($i < $count && is_array($tokens[$i]) && T_STRING === $tokens[$i][0]) { 311 | $name .= '\\'.$tokens[$i][1]; 312 | $i += 2; 313 | } 314 | 315 | $newTokens[] = [T_NAME_FULLY_QUALIFIED, $name, $token[2]]; 316 | } elseif (is_array($token) && T_STRING === $token[0]) { 317 | ++$i; 318 | $name = $token[1]; 319 | $qualified = false; 320 | 321 | while ($i < $count && is_array($tokens[$i]) && T_NS_SEPARATOR === $tokens[$i][0]) { 322 | $qualified = true; 323 | $name .= '\\'.$tokens[$i + 1][1]; 324 | $i += 2; 325 | } 326 | 327 | if ($qualified) { 328 | $newTokens[] = [T_NAME_FULLY_QUALIFIED, '\\'.$name, $token[2]]; 329 | } else { 330 | $newTokens[] = [T_STRING, $name, $token[2]]; 331 | } 332 | } else { 333 | ++$i; 334 | $newTokens[] = $token; 335 | } 336 | } 337 | 338 | for ($i = 0; $i < count($newTokens); ++$i) { 339 | if (!isset($newTokens[$i - 1]) || !isset($newTokens[$i]) || !isset($newTokens[$i + 1])) { 340 | continue; 341 | } 342 | 343 | if (!in_array($newTokens[$i - 1][0], [T_STRING, T_NAME_QUALIFIED, T_NAME_FULLY_QUALIFIED]) || 344 | T_DOUBLE_COLON !== $newTokens[$i][0] || 345 | T_STRING !== $newTokens[$i + 1][0] 346 | ) { 347 | continue; 348 | } 349 | 350 | $token = $newTokens[$i - 1][1].$newTokens[$i][1].$newTokens[$i + 1][1]; 351 | $newTokens[$i - 1] = [X_STATIC_METHOD, $token, $newTokens[$i + 1][2]]; 352 | unset($newTokens[$i], $newTokens[$i + 1]); 353 | ++$i; 354 | } 355 | 356 | return array_values($newTokens); 357 | } 358 | } 359 | -------------------------------------------------------------------------------- /src/Parser/Reflection.php: -------------------------------------------------------------------------------- 1 | reflection = new \ReflectionFunction($funcOrClassOrObject); 22 | } 23 | 24 | $className = is_object($funcOrClassOrObject) ? get_class($funcOrClassOrObject) : $funcOrClassOrObject; 25 | if (class_exists($className) || interface_exists($className)) { 26 | $this->reflection = new \ReflectionClass($className); 27 | } 28 | 29 | /* 30 | * it cannot proceed without function or class source file 31 | */ 32 | if ($this->reflection && !$this->getFilePath()) { 33 | $this->reflection = null; 34 | } 35 | } 36 | 37 | public static function init($funcOrClassOrObject): self 38 | { 39 | return new self($funcOrClassOrObject); 40 | } 41 | 42 | public static function getFunction($functionName): array 43 | { 44 | return self::init($functionName)->getFunctionInfo(); 45 | } 46 | 47 | public static function getClass($objectOrClass): array 48 | { 49 | return self::init($objectOrClass)->getClassInfo(); 50 | } 51 | 52 | /** 53 | * Get merged class from type like, Foo|Bar, Foo&Bar. 54 | * 55 | * @param string $type union and intersection type 56 | * 57 | * @return array|array[] 58 | */ 59 | public static function getClassFromType(string $type, ?string $name = null): array 60 | { 61 | if (false !== strpos($type, '|')) { 62 | $foundTypes = explode('|', $type); 63 | } elseif (false !== strpos($type, '&')) { 64 | $foundTypes = explode('&', $type); 65 | } else { 66 | $foundTypes = [$type]; 67 | } 68 | 69 | $classes = []; 70 | foreach ($foundTypes as $foundType) { 71 | if (!$class = self::getClass($foundType)) { 72 | continue; 73 | } 74 | 75 | if (!$classes) { 76 | $classes = $class; 77 | } elseif (false !== strpos($type, '|')) { 78 | $classes = self::getUnionArray($classes, $class); 79 | } elseif (false !== strpos($type, '&')) { 80 | $classes = self::getIntersectedArray($classes, $class); 81 | } else { 82 | $classes = $class; 83 | } 84 | } 85 | 86 | if (!$name || !$classes) { 87 | return $classes; 88 | } 89 | 90 | foreach (['staticProperties', 'properties', 'staticMethods', 'methods'] as $identifier) { 91 | foreach ($classes[$identifier] as $value) { 92 | if ($value['name'] === $name) { 93 | $value['identifier'] = in_array($identifier, ['staticMethods', 'methods']) ? 'method' : 'prop'; 94 | 95 | return $value; 96 | } 97 | } 98 | } 99 | 100 | return []; 101 | } 102 | 103 | public static function getClassItemFromType(string $type, string $name): array 104 | { 105 | if (!$type || !$name) { 106 | return []; 107 | } 108 | 109 | if ($class = self::getClassFromType($type)) { 110 | foreach (['staticProperties', 'properties', 'staticMethods', 'methods'] as $identifier) { 111 | foreach ($class[$identifier] as $value) { 112 | if ($value['name'] === $name) { 113 | $value['identifier'] = in_array($identifier, ['staticMethods', 'methods']) ? 'method' : 'property'; 114 | 115 | return $value; 116 | } 117 | } 118 | } 119 | } 120 | 121 | return []; 122 | } 123 | 124 | private function getFunctionInfo(): array 125 | { 126 | if (!$this->reflection instanceof \ReflectionFunction) { 127 | return []; 128 | } 129 | 130 | $functionName = $this->reflection->getName(); 131 | if (isset(self::$cache[$functionName])) { 132 | return self::$cache[$functionName]; 133 | } 134 | 135 | $info = [ 136 | 'name' => $functionName, 137 | 'doc' => DocParser::parse($doc = $this->getDoc($this->reflection))->removeTag('@source')->print(), 138 | 'signature' => SignatureFormatter::format($this->reflection), 139 | 'type' => $this->getType($this->reflection->getReturnType(), $doc, $this->reflection), 140 | ]; 141 | 142 | return self::$cache[$functionName] = $info; 143 | } 144 | 145 | private function getClassInfo(): array 146 | { 147 | if (!$this->reflection instanceof \ReflectionClass) { 148 | return []; 149 | } 150 | 151 | $className = $this->reflection->getName(); 152 | if (isset(self::$cache[$className])) { 153 | return self::$cache[$className]; 154 | } 155 | 156 | $classInfo = [ 157 | 'staticProperties' => [], 'properties' => [], 158 | 'staticMethods' => [], 'methods' => [], 159 | ]; 160 | 161 | $properties = $this->reflection->getProperties(\ReflectionProperty::IS_PUBLIC); 162 | $methods = $this->reflection->getMethods(\ReflectionMethod::IS_PUBLIC); 163 | 164 | foreach ($properties as $property) { 165 | $info = []; 166 | $info['class'] = $className; 167 | $info['name'] = $property->getName(); 168 | $info['doc'] = DocParser::parse($doc = $this->getDoc($property))->removeTag('@source')->print(); 169 | $info['signature'] = SignatureFormatter::format($property); 170 | $info['type'] = $this->getType(method_exists($property, 'getType') ? $property->getType() : '', $doc, $property); 171 | 172 | $property->isStatic() ? $classInfo['staticProperties'][] = $info : $classInfo['properties'][] = $info; 173 | } 174 | 175 | foreach ($methods as $method) { 176 | $info = []; 177 | $info['class'] = $className; 178 | $info['name'] = $method->getName(); 179 | $info['doc'] = DocParser::parse($doc = $this->getDoc($method))->removeTag('@source')->print(); 180 | $info['signature'] = SignatureFormatter::format($method); 181 | $info['type'] = $this->getType($method->getReturnType(), $doc, $method); 182 | 183 | $method->isStatic() ? $classInfo['staticMethods'][] = $info : $classInfo['methods'][] = $info; 184 | } 185 | 186 | return self::$cache[$className] = $classInfo; 187 | } 188 | 189 | /** 190 | * @param \ReflectionFunction|\ReflectionProperty|\ReflectionMethod $reflection 191 | */ 192 | private function getDoc($reflection, $sourceClass = ''): string 193 | { 194 | $name = $reflection->getName(); 195 | 196 | if (!$doc = $reflection->getDocComment() ?: '') { 197 | $filePath = method_exists($reflection, 'getFileName') ? $reflection->getFileName() : false; 198 | $filePath = $filePath ?: $this->getFilePath(); 199 | $doc = $this->getNodes($filePath)['docComments'][$name] ?? ''; 200 | } 201 | 202 | if ($reflection instanceof \ReflectionFunction) { 203 | return $this->trimDoc($doc); 204 | } 205 | 206 | $classReflection = $reflection->getDeclaringClass(); 207 | $parent = $classReflection->getParentClass() ? [$classReflection->getParentClass()] : []; 208 | $parents = array_merge($parent, $classReflection->getInterfaces()); 209 | 210 | if (!$parents || ($doc && 1 !== preg_match('/@inheritdoc\b/i', $doc))) { 211 | if ($sourceClass) { 212 | $doc = DocParser::parse($doc)->addTag('@source', $sourceClass)->print(); 213 | } 214 | 215 | return $this->trimDoc($doc); 216 | } 217 | 218 | foreach ($parents as $parent) { 219 | if ($this->doesThisClassHaveMethod($parent, $name)) { 220 | return $this->getDoc($parent->getMethod($name), $parent->getName()); 221 | } 222 | 223 | if ($this->doesThisClassHaveProperty($parent, $name)) { 224 | return $this->getDoc($parent->getProperty($name), $parent->getName()); 225 | } 226 | 227 | $grandParent = $parent->getParentClass() ? [$parent->getParentClass()] : []; 228 | $grandParents = array_merge($grandParent, $parent->getInterfaces()); 229 | 230 | foreach ($grandParents as $grandParent) { 231 | if ($parent->hasMethod($name) && $grandParent->hasMethod($name)) { 232 | return $this->getDoc($grandParent->getMethod($name), $grandParent->getName()); 233 | } 234 | 235 | if ($parent->hasProperty($name) && $grandParent->hasProperty($name)) { 236 | return $this->getDoc($grandParent->getProperty($name), $grandParent->getName()); 237 | } 238 | } 239 | } 240 | 241 | return $this->trimDoc($doc); 242 | } 243 | 244 | private function doesThisClassHaveMethod($classOrInterface, $name): bool 245 | { 246 | if ($classOrInterface->hasMethod($name)) { 247 | $method = $classOrInterface->getMethod($name); 248 | 249 | return $method->getDeclaringClass()->getName() === $classOrInterface->getName(); 250 | } 251 | 252 | return false; 253 | } 254 | 255 | private function doesThisClassHaveProperty($classOrInterface, $name): bool 256 | { 257 | if ($classOrInterface->hasProperty($name)) { 258 | $method = $classOrInterface->getProperty($name); 259 | 260 | return $method->getDeclaringClass()->getName() === $classOrInterface->getName(); 261 | } 262 | 263 | return false; 264 | } 265 | 266 | private function trimDoc(string $doc): string 267 | { 268 | return preg_replace('/\n\s+\*/', "\n *", trim($doc)); 269 | } 270 | 271 | /** 272 | * @param $reflection \ReflectionFunction|\ReflectionMethod|\ReflectionProperty 273 | */ 274 | private function getType($types, $doc, $reflection): string 275 | { 276 | if (!$types && !$doc) { 277 | return ''; 278 | } 279 | 280 | if ($types) { 281 | $separator = $types instanceof \ReflectionUnionType ? '|' : ($types instanceof \ReflectionIntersectionType ? '&' : ' '); 282 | if (in_array($separator, ['|', '&'])) { 283 | $foundTypes = $types->getTypes(); 284 | } elseif ($types instanceof \ReflectionNamedType) { 285 | $foundTypes = [$types]; 286 | } else { 287 | return ''; 288 | } 289 | $foundTypes = array_map(function ($type) { 290 | return $this->getFullClassName($type->getName()); 291 | }, $foundTypes); 292 | 293 | return implode($separator, $foundTypes); 294 | } 295 | 296 | $parser = DocParser::parse($doc); 297 | $types = $reflection instanceof \ReflectionProperty ? $parser->getPropertyType() : $parser->getReturnType(); 298 | 299 | if ($types) { 300 | $separator = $types instanceof UnionTypeNode ? '|' : ($types instanceof IntersectionTypeNode ? '&' : ' '); 301 | $types = preg_replace('/\(|\)|(<[^>]+>)/', '', (string) $types); 302 | $foundTypes = explode($separator, $types); 303 | $source = $parser->getValue('@source'); 304 | $foundTypes = array_map(function ($type) use ($source) { 305 | return $this->getFullClassName($type, $source); 306 | }, $foundTypes); 307 | 308 | return implode($separator, $foundTypes); 309 | } 310 | 311 | return ''; 312 | } 313 | 314 | private function getNodesByClass(string $className): array 315 | { 316 | if (!class_exists($className) && !interface_exists($className)) { 317 | return []; 318 | } 319 | $reflection = new \ReflectionClass($className); 320 | 321 | return $this->getNodes($reflection->getFileName()); 322 | } 323 | 324 | private function getNodes(?string $filePath = null): array 325 | { 326 | $filePath = $filePath ?: $this->getFilePath(); 327 | 328 | if ($nodes = self::$cacheFilesNodes[$filePath] ?? []) { 329 | return $nodes; 330 | } 331 | 332 | $code = file_get_contents($filePath); 333 | 334 | $parser = (new ParserFactory())->createForNewestSupportedVersion(); 335 | $ast = $parser->parse($code); 336 | $traverser = new NodeTraverser(); 337 | $nodeVisitor = new NodeVisitor(); 338 | $traverser->addVisitor($nodeVisitor); 339 | $traverser->traverse($ast); 340 | 341 | $nodes = [ 342 | 'namespace' => $nodeVisitor->namespace, 343 | 'className' => $nodeVisitor->className, 344 | 'usedClasses' => $nodeVisitor->usedClasses, 345 | 'docComments' => $nodeVisitor->docComments, 346 | ]; 347 | 348 | return self::$cacheFilesNodes[$filePath] = $nodes; 349 | } 350 | 351 | private function getFilePath(): string 352 | { 353 | if ($path = $this->reflection->getFileName()) { 354 | return $path; 355 | } 356 | 357 | $name = $this->reflection->getName(); 358 | $dir = PhpStormStubsMap::DIR; 359 | $functions = PhpStormStubsMap::FUNCTIONS; 360 | $classes = PhpStormStubsMap::CLASSES; 361 | $path = $classes[$name] ?? $functions[$name] ?? ''; 362 | 363 | if (file_exists($path = $dir.'/'.$path)) { 364 | return $path; 365 | } 366 | 367 | return ''; 368 | } 369 | 370 | private function getFullClassName(string $class, ?string $source = null) 371 | { 372 | $class = trim($class); 373 | $nodes = $source ? $this->getNodesByClass($source) : $this->getNodes(); 374 | 375 | if (!$nodes) { 376 | return $class; 377 | } 378 | 379 | if (in_array($class, ['this', '$this', 'self', 'static'])) { 380 | return $nodes['className'] ?? $class; 381 | } 382 | 383 | $filter = array_filter($nodes['usedClasses'], function ($usedClass) use ($class) { 384 | return substr($usedClass, -strlen($class)) === $class; 385 | }); 386 | 387 | $className = $nodes['namespace'] ? $nodes['namespace'].'\\'.$class : $class; 388 | if (!$filter && (class_exists($className) || interface_exists($className))) { 389 | $class = $className; 390 | } 391 | 392 | return reset($filter) ?: $class; 393 | } 394 | 395 | private static function getUnionArray(array $arrayA, array $arrayB): array 396 | { 397 | $merged = array_merge_recursive($arrayA, $arrayB); 398 | 399 | foreach ($merged as $key => $valueArr) { 400 | $tmpArr = array_unique(array_column($valueArr, 'name')); 401 | $merged[$key] = array_intersect_key($valueArr, $tmpArr); 402 | } 403 | 404 | return $merged; 405 | } 406 | 407 | private static function getIntersectedArray(array $arrayA, array $arrayB): array 408 | { 409 | $intersected = []; 410 | foreach (array_keys($arrayA) as $key) { 411 | $intersected[$key] = []; 412 | foreach ($arrayA[$key] as $a) { 413 | foreach ($arrayB[$key] as $b) { 414 | if ($a['name'] === $b['name']) { 415 | $intersected[$key][] = $a; 416 | } 417 | } 418 | } 419 | } 420 | 421 | return $intersected; 422 | } 423 | } 424 | --------------------------------------------------------------------------------