Skip to content

Resolve - PathAutoCompletion causes timeouts on large projects #27

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion src/generator/ApiGenerator.php
Original file line number Diff line number Diff line change
Expand Up @@ -372,7 +372,8 @@ public function hints()
*/
public function autoCompleteData()
{
return (new PathAutoCompletion())->complete();
$config = $this->makeConfig();
return (new PathAutoCompletion($config))->complete();
}

/**
Expand Down
120 changes: 88 additions & 32 deletions src/lib/PathAutoCompletion.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,45 +11,31 @@
use RecursiveIteratorIterator;
use RegexIterator;
use Throwable;
use cebe\yii2openapi\lib\Config;
use Yii;
use yii\helpers\FileHelper;

class PathAutoCompletion
{
public function complete():array
{
$vendor = Yii::getAlias('@vendor');
$app = Yii::getAlias('@app');
$runtime = Yii::getAlias('@runtime');
$paths = [];
$pathIterator = new RecursiveDirectoryIterator($app);
$recursiveIterator = new RecursiveIteratorIterator($pathIterator);
$files = new RegexIterator($recursiveIterator, '~.+\.(json|yaml|yml)$~i', RegexIterator::GET_MATCH);
foreach ($files as $file) {
if (strpos($file[0], $vendor) === 0) {
$file = FileHelper::normalizePath('@vendor' . substr($file[0], strlen($vendor)));
} elseif (strpos($file[0], $runtime) === 0) {
$file = null;
} elseif (strpos($file[0], $app) === 0) {
$file = FileHelper::normalizePath('@app' . substr($file[0], strlen($app)));
} else {
$file = $file[0];
}

if ($file !== null) {
$paths[] = $file;
}
}
/**
* @var ?Config
*/
private $_config;

$namespaces = array_merge(...array_map([$this, 'completeAlias'], array_keys(Yii::$aliases)));
public function __construct(?Config $config = null)
{
$this->_config = $config;
}

public function complete():array
{
return [
'openApiPath' => $paths,
'controllerNamespace' => $namespaces,
'modelNamespace' => $namespaces,
'fakerNamespace' => $namespaces,
'migrationNamespace' => $namespaces,
'transformerNamespace' => $namespaces,
'openApiPath' => $this->computePaths($this->_config),
'controllerNamespace' => $this->computeNamesapces('controllerNamespace'),
'modelNamespace' => $this->computeNamesapces('modelNamespace'),
'fakerNamespace' => $this->computeNamesapces('fakerNamespace'),
'migrationNamespace' => $this->computeNamesapces('migrationNamespace'),
'transformerNamespace' => $this->computeNamesapces('transformerNamespace'),
// 'urlConfigFile' => [
// '@app/config/urls.rest.php',
// ],
Expand All @@ -66,7 +52,7 @@ private function completeAlias(string $alias):array
return [];
}
try {
$dirs = FileHelper::findDirectories($path, ['except' => ['vendor/','runtime/','assets/','.git/','.svn/']]);
$dirs = FileHelper::findDirectories($path, ['except' => ['vendor/','runtime/','assets/','.git/','.svn/', '/web']]);
} catch (Throwable $e) {
// ignore errors with file permissions
Yii::error($e);
Expand All @@ -76,4 +62,74 @@ private function completeAlias(string $alias):array
return str_replace('/', '\\', substr($alias, 1) . substr($dir, strlen($path)));
}, $dirs);
}

private function computeNamesapces(string $property): array
{
$config = $this->_config;
if ($config && $config->$property) {
return [$config->$property];
}

$key = 'cebe-yii2-openapi-autocompletion-data-namespaces';
$list = Yii::$app->cache->get($key);
if ($list !== false) {
return $list;
}
$list = array_merge(...array_map([$this, 'completeAlias'], array_keys(Yii::$aliases)));
Yii::$app->cache->set($key, $list, 3*24*60*60); // 3 days
return $list;
}

private function computePaths(): array
{
$config = $this->_config;

// First priority will be given to values present in config (example) to be shown in form fields.
// Second to default values present in class cebe\yii2openapi\generator\ApiGenerator
// Third will be given to values produced by PathAutoCompletion class

if ($config && $config->openApiPath) {
return [$config->openApiPath];
}

// check it is present in cache
$key = 'cebe-yii2-openapi-autocompletion-data-paths';
// use cache
$list = Yii::$app->cache->get($key);
if ($list !== false) {
return $list;
}

// 3rd priority
$vendor = Yii::getAlias('@vendor');
$webroot = Yii::getAlias('@webroot');
$tests = Yii::getAlias('@app/tests');
$app = Yii::getAlias('@app');
$runtime = Yii::getAlias('@runtime');
$paths = [];
$pathIterator = new RecursiveDirectoryIterator($app);
$recursiveIterator = new RecursiveIteratorIterator($pathIterator);
$files = new RegexIterator($recursiveIterator, '~.+\.(json|yaml|yml)$~i', RegexIterator::GET_MATCH);
foreach ($files as $file) {
if (strpos($file[0], $vendor) === 0) {
$file = null;
} elseif (strpos($file[0], $tests) === 0) {
$file = null;
} elseif (strpos($file[0], $webroot) === 0) {
$file = null;
} elseif (strpos($file[0], $runtime) === 0) {
$file = null;
} elseif (strpos($file[0], $app) === 0) {
$file = FileHelper::normalizePath('@app' . substr($file[0], strlen($app)));
} else {
$file = $file[0];
}

if ($file !== null) {
$paths[] = $file;
}
}
Yii::$app->cache->set($key, $paths, 3*24*60*60); // 3 days
return $paths;
}
}
3 changes: 3 additions & 0 deletions tests/config/console.php
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,9 @@
'charset' => 'utf8',
'tablePrefix'=>'itt_',
],
'cache' => [
'class' => 'yii\caching\FileCache',
],
],
];

Expand Down
36 changes: 30 additions & 6 deletions tests/unit/PathAutoCompletionTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,19 +3,15 @@
namespace tests\unit;

use cebe\yii2openapi\lib\PathAutoCompletion;
use cebe\yii2openapi\lib\Config;
use tests\TestCase;
use Yii;

class PathAutoCompletionTest extends TestCase
{

public function testComplete()
{
Yii::setAlias('@vendor', __DIR__.'/items');
$this->prepareTempDir();
Yii::setAlias('@app', __DIR__.'/../specs');
Yii::setAlias('@runtime', __DIR__.'/../tmp/app');

$this->registerApp();

$completion = (new PathAutoCompletion())->complete();
self::assertNotEmpty($completion);
Expand All @@ -28,4 +24,32 @@ public function testComplete()
self::assertContains('@app/blog.yaml', $completion['openApiPath']);
self::assertContains('@app/petstore.yaml', $completion['openApiPath']);
}

public function testCompletionFromConfigAndDefault()
{
$this->registerApp();

$completion = (new PathAutoCompletion(new Config([
'openApiPath' => '@root/openapi/schema.yaml',
'controllerNamespace' => 'api\\controllers',
])))->complete();

self::assertNotEmpty($completion);
self::assertArrayHasKey('openApiPath', $completion);
self::assertSame(['@root/openapi/schema.yaml'], $completion['openApiPath']);
self::assertSame(['api\\controllers'], $completion['controllerNamespace']);
self::assertSame(['app\\models'], $completion['modelNamespace']);
self::assertContains('yii\messages\sl', $completion['migrationNamespace']);
}

private function registerApp()
{
Yii::setAlias('@vendor', __DIR__.'/items');
$this->prepareTempDir();
Yii::setAlias('@runtime', __DIR__.'/../tmp/app');

$this->mockRealApplication(); // to register cache component
Yii::setAlias('@app', __DIR__.'/../specs');
Yii::setAlias('@webroot', __DIR__.'@app/web');
}
}