├── .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', '', $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 | 
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 | 
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 | 
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 | 
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 | 
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()->[37;41mdoesNotExist[39;49m->\n", '']],
87 |
88 | ['foo()->bar()->', ['foo()', 'fooBar()', 'fooDocBar()']],
89 | ['foo()->bar->', ["\n ** Invalid method closing: foo()->[37;41mbar[39;49m->\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 | [32m/**
138 | * A global user-defined function for testing
139 | *
140 | * [39m[35m@return[39m[32m Foo
141 | */[39m
142 | [36mfunction[39m [31mfoo[39m()
143 |
144 | DOC,
145 | 'inheritdoc' => <<<'DOC'
146 | [32m/**
147 | * You're following inheritdoc to get me
148 | *
149 | * [39m[35m@return[39m[32m Foo
150 | */[39m
151 | [36mpublic function[39m [31mfoo[39m()
152 |
153 | DOC,
154 | 'min' => <<<'DOC'
155 | [32m/**
156 | * Find lowest value
157 | * [39m[35m@link[39m[32m https://php.net/manual/en/function.min.php
158 | * [39m[35m@param[39m[32m array|mixed [39m[31m$value[39m[32m Array to look through or first value to compare
159 | * [39m[35m@param[39m[32m mixed ...[39m[31m$values[39m[32m any comparable value
160 | * [39m[35m@return[39m[32m mixed min returns the numerically lowest of the
161 | * parameter values.
162 | */[39m
163 | [36mfunction[39m [31mmin[39m(?[36mmixed [39m[31m$value[39m, ?[36mmixed [39m...[31m$values[39m)
164 |
165 | DOC,
166 | 'noReturn' => <<<'DOC'
167 | [32m/**
168 | * it has no return type
169 | */[39m
170 | [36mpublic function[39m [31mnoReturn[39m()
171 |
172 | DOC,
173 | 'sql' => <<<'DOC'
174 | [32m/**
175 | * Executes a raw SQL query and returns the result.
176 | *
177 | * [39m[35m@param[39m[32m string [39m[31m$sql[39m[32m the SQL query string
178 | * [39m[35m@param[39m[32m array [39m[31m$params[39m[32m an associative array of query parameters
179 | *
180 | * [39m[35m@return[39m[32m array[]|void
181 | */[39m
182 | [36mfunction[39m [31msql[39m([36mstring [39m[31m$sql[39m, [36marray [39m[31m$params[39m = [])
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 |
--------------------------------------------------------------------------------
|