Skip to content
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

プラグインのライフサイクルとエンティティProxy再生成/スキーマ更新タイミングの調整 #2546

Merged
merged 2 commits into from
Oct 11, 2017
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
1 change: 1 addition & 0 deletions src/Eccube/Command/GenerateProxyCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ protected function execute(InputInterface $input, OutputInterface $output)

$entityProxyService->generate(
array_merge([$app['config']['root_dir'].'/app/Acme/Entity'], $dirs),
[],
$app['config']['root_dir'].'/app/proxy/entity',
$output
);
Expand Down
157 changes: 100 additions & 57 deletions src/Eccube/Service/EntityProxyService.php
Original file line number Diff line number Diff line change
Expand Up @@ -50,55 +50,26 @@ class EntityProxyService
/**
* EntityのProxyを生成します。
*
* @param array $scanDirs スキャン対象ディレクトリ
* @param array $includesDirs Proxyに含めるTraitがあるディレクトリ一覧
* @param array $excludeDirs Proxyから除外するTraitがあるディレクトリ一覧
* @param string $outputDir 出力先
* @param OutputInterface $output ログ出力
* @return array 生成したファイルのリスト
*/
public function generate($scanDirs, $outputDir, OutputInterface $output = null)
public function generate($includesDirs, $excludeDirs, $outputDir, OutputInterface $output = null)
{
if (is_null($output)) {
$output = new ConsoleOutput();
}

// Acmeからファイルを抽出
$files = Finder::create()
->in(array_filter($scanDirs, 'file_exists'))
->name('*.php')
->files();

// traitの一覧を取得
$traits = [];
$includedFiles = [];
foreach ($files as $file) {
require_once $file->getRealPath();
$includedFiles[] = $file->getRealPath();
}

$declared = get_declared_traits();

foreach ($declared as $className) {
$rc = new \ReflectionClass($className);
$sourceFile = $rc->getFileName();
if (in_array($sourceFile, $includedFiles)) {
$traits[] = $className;
}
}

// traitから@EntityExtensionを抽出
$reader = new AnnotationReader();
$proxies = [];
foreach ($traits as $trait) {
$anno = $reader->getClassAnnotation(new \ReflectionClass($trait), EntityExtension::class);
if ($anno) {
$proxies[$anno->value][] = $trait;
}
}

$generatedFiles = [];

list($addTraits, $removeTrails) = $this->scanTraits([$includesDirs, $excludeDirs]);
$targetEntities = array_unique(array_merge(array_keys($addTraits), array_keys($removeTrails)));

// プロキシファイルの生成
foreach ($proxies as $targetEntity => $traits) {
foreach ($targetEntities as $targetEntity) {
$traits = isset($addTraits[$targetEntity]) ? $addTraits[$targetEntity] : [];
$rc = new ClassReflection($targetEntity);
$generator = ClassGenerator::fromReflection($rc);
$uses = FileGenerator::fromReflectedFileName($rc->getFileName())->getUses();
Expand All @@ -107,27 +78,15 @@ public function generate($scanDirs, $outputDir, OutputInterface $output = null)
$generator->addUse($use[0], $use[1]);
}

foreach ($traits as $trait) {
$rt = new ClassReflection($trait);
foreach ($rt->getProperties() as $prop) {
// すでにProxyがある場合, $generatorにuse XxxTrait;が存在せず,
// traitに定義されているフィールド,メソッドがクラス側に追加されてしまう
if ($generator->hasProperty($prop->getName())) {
// $generator->removeProperty()はzend-code 2.6.3 では未実装なのでリフレクションで削除.
$generatorRefObj = new \ReflectionObject($generator);
$generatorRefProp = $generatorRefObj->getProperty('properties');
$generatorRefProp->setAccessible(true);
$properies = $generatorRefProp->getValue($generator);
unset($properies[$prop->getName()]);
$generatorRefProp->setValue($generator, $properies);
}
}
foreach ($rt->getMethods() as $method) {
if ($generator->hasMethod($method->getName())) {
$generator->removeMethod($method->getName());
}
if (isset($removeTrails[$targetEntity])) {
foreach ($removeTrails[$targetEntity] as $trait) {
$this->removePropertiesFromProxy($trait, $generator);
}
$generator->addTrait('\\'.$trait);
}

foreach ($traits as $trait) {
$this->removePropertiesFromProxy($trait, $generator);
$generator->addTrait('\\' . $trait);
}

// extendしたクラスが相対パスになるので
Expand All @@ -151,4 +110,88 @@ public function generate($scanDirs, $outputDir, OutputInterface $output = null)

return $generatedFiles;
}

/**
* 複数のディレクトリセットをスキャンしてディレクトリセットごとのEntityとTraitのマッピングを返します.
* @param $dirSets array スキャン対象ディレクトリリストの配列
* @return array ディレクトリセットごとのEntityとTraitのマッピング
*/
private function scanTraits($dirSets)
{
// ディレクトリセットごとのファイルをロードしつつ一覧を作成
$includedFileSets = [];
foreach ($dirSets as $dirSet) {
$includedFiles = [];
$dirs = array_filter($dirSet, 'file_exists');
if (!empty($dirs)) {
$files = Finder::create()
->in($dirs)
->name('*.php')
->files();

foreach ($files as $file) {
require_once $file->getRealPath();
$includedFiles[] = $file->getRealPath();
}
}
$includedFileSets[] = $includedFiles;
}

$declaredTraits = get_declared_traits();

// ディレクトリセットに含まれるTraitの一覧を作成
$traitSets = array_map(function() { return []; }, $dirSets);
foreach ($declaredTraits as $className) {
$rc = new \ReflectionClass($className);
$sourceFile = $rc->getFileName();
foreach ($includedFileSets as $index=>$includedFiles) {
if (in_array($sourceFile, $includedFiles)) {
$traitSets[$index][] = $className;
}
}
}

// TraitをEntityごとにまとめる
$reader = new AnnotationReader();
$proxySets = [];
foreach ($traitSets as $traits) {
$proxies = [];
foreach ($traits as $trait) {
$anno = $reader->getClassAnnotation(new \ReflectionClass($trait), EntityExtension::class);
if ($anno) {
$proxies[$anno->value][] = $trait;
}
}
$proxySets[] = $proxies;
}

return $proxySets;
}

/**
* @param $trait
* @param $generator
*/
private function removePropertiesFromProxy($trait, $generator)
{
$rt = new ClassReflection($trait);
foreach ($rt->getProperties() as $prop) {
// すでにProxyがある場合, $generatorにuse XxxTrait;が存在せず,
// traitに定義されているフィールド,メソッドがクラス側に追加されてしまう
if ($generator->hasProperty($prop->getName())) {
// $generator->removeProperty()はzend-code 2.6.3 では未実装なのでリフレクションで削除.
$generatorRefObj = new \ReflectionObject($generator);
$generatorRefProp = $generatorRefObj->getProperty('properties');
$generatorRefProp->setAccessible(true);
$properies = $generatorRefProp->getValue($generator);
unset($properies[$prop->getName()]);
$generatorRefProp->setValue($generator, $properies);
}
}
foreach ($rt->getMethods() as $method) {
if ($generator->hasMethod($method->getName())) {
$generator->removeMethod($method->getName());
}
}
}
}
69 changes: 54 additions & 15 deletions src/Eccube/Service/PluginService.php
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,11 @@ public function install($path, $source = 0)
$pluginBaseDir = null;
$tmp = null;

// Proxyのクラスをロードせずにスキーマを更新するために、
// インストール時には一時的なディレクトリにProxyを生成する
$tmpProxyOutputDir = sys_get_temp_dir() . '/proxy_' . Str::random(12);
@mkdir($tmpProxyOutputDir);

try {
PluginConfigManager::removePluginConfigCache();
Cache::clear($this->app, false);
Expand All @@ -114,7 +119,12 @@ public function install($path, $source = 0)

$this->unpackPluginArchive($path, $pluginBaseDir); // 問題なければ本当のplugindirへ

$this->registerPlugin($config, $event, $source); // dbにプラグイン登録
$plugin = $this->registerPlugin($config, $event, $source); // dbにプラグイン登録

// インストール時には一時的に利用するProxyを生成してからスキーマを更新する
$generatedFiles = $this->regenerateProxy($plugin, true, $tmpProxyOutputDir);
$this->schemaService->updateSchema($generatedFiles, $tmpProxyOutputDir);

ConfigManager::writePluginConfigCache();
} catch (PluginException $e) {
$this->deleteDirs(array($tmp, $pluginBaseDir));
Expand All @@ -123,6 +133,11 @@ public function install($path, $source = 0)

$this->deleteDirs(array($tmp, $pluginBaseDir));
throw $e;
} finally {
foreach (glob("${tmpProxyOutputDir}/*") as $f) {
unlink($f);
}
rmdir($tmpProxyOutputDir);
}

return true;
Expand Down Expand Up @@ -323,6 +338,10 @@ public function uninstall(\Eccube\Entity\Plugin $plugin)
$this->disable($plugin);
$this->unregisterPlugin($plugin);
$this->deleteFile($pluginDir);

// スキーマを更新する
$this->schemaService->updateSchema([], $this->appConfig['root_dir'].'/app/proxy/entity');

ConfigManager::writePluginConfigCache();
return true;
}
Expand All @@ -346,25 +365,44 @@ public function disable(\Eccube\Entity\Plugin $plugin)
return $this->enable($plugin, false);
}

private function regenerateProxy(Plugin $plugin)
/**
* Proxyを再生成します.
* @param Plugin $plugin プラグイン
* @param boolean $temporary プラグインが無効状態でも一時的に生成するかどうか
* @param string|null $outputDir 出力先
* @return array 生成されたファイルのパス
*/
private function regenerateProxy(Plugin $plugin, $temporary, $outputDir = null)
{
$enabledPluginEntityDirs = array_map(function($p) {
return $this->appConfig['root_dir'].'/app/Plugin/'.$p->getCode().'/Entity';
}, $this->pluginRepository->findAllEnabled());
if (is_null($outputDir)) {
$outputDir = $this->appConfig['root_dir'].'/app/proxy/entity';
}
@mkdir($outputDir);

$enabledPluginCodes = array_map(
function($p) { return $p->getCode(); },
$this->pluginRepository->findAllEnabled()
);

$entityDir = $this->appConfig['root_dir'].'/app/Plugin/'.$plugin->getCode().'/Entity';
if ($plugin->getEnable() === Constant::ENABLED) {
$enabledPluginEntityDirs[] = $entityDir;
$excludes = [];
if ($temporary || $plugin->getEnable() === Constant::ENABLED) {
$enabledPluginCodes[] = $plugin->getCode();
} else {
$index = array_search($entityDir, $enabledPluginEntityDirs);
if ($index >=0 ) {
array_splice($enabledPluginEntityDirs, $index, 1);
$index = array_search($plugin->getCode(), $enabledPluginCodes);
if ($index >= 0) {
array_splice($enabledPluginCodes, $index, 1);
$excludes = [$this->appConfig['root_dir']."/app/Plugin/".$plugin->getCode()."/Entity"];
}
}

$enabledPluginEntityDirs = array_map(function($code) {
return $this->appConfig['root_dir']."/app/Plugin/${code}/Entity";
}, $enabledPluginCodes);

return $this->entityProxyService->generate(
array_merge([$this->appConfig['root_dir'].'/app/Acme/Entity'], $enabledPluginEntityDirs),
$this->appConfig['root_dir'].'/app/proxy/entity'
$excludes,
$outputDir
);
}

Expand All @@ -379,10 +417,11 @@ public function enable(\Eccube\Entity\Plugin $plugin, $enable = true)
$plugin->setEnable($enable ? Constant::ENABLED : Constant::DISABLED);
$em->persist($plugin);

$generatedFiles = $this->regenerateProxy($plugin);
$this->schemaService->updateSchema($generatedFiles);

$this->callPluginManagerMethod(Yaml::parse(file_get_contents($pluginDir.'/'.self::CONFIG_YML)), $enable ? 'enable' : 'disable');

// Proxyだけ再生成してスキーマは更新しない
$this->regenerateProxy($plugin, false);

$em->flush();
$em->getConnection()->commit();
PluginConfigManager::writePluginConfigCache();
Expand Down
4 changes: 2 additions & 2 deletions src/Eccube/Service/SchemaService.php
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ class SchemaService
*/
protected $entityManager;

public function updateSchema($generatedFiles)
public function updateSchema($generatedFiles, $proxiesDirectory)
{
$outputDir = sys_get_temp_dir() . '/proxy_' . Str::random(12);
mkdir($outputDir);
Expand All @@ -58,7 +58,7 @@ public function updateSchema($generatedFiles)
);
$newDriver->setFileExtension($oldDriver->getFileExtension());
$newDriver->addExcludePaths($oldDriver->getExcludePaths());
$newDriver->setTraitProxiesDirectory(realpath(__DIR__.'/../../../app/proxy/entity'));
$newDriver->setTraitProxiesDirectory($proxiesDirectory);
$newDriver->setNewProxyFiles($generatedFiles);
$newDriver->setOutputDir($outputDir);
$chain->addDriver($newDriver, $namespace);
Expand Down
35 changes: 32 additions & 3 deletions tests/Eccube/Tests/Service/EntityProxyServiceTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ protected function tearDown()
public function testGenerate()
{
$generator = new EntityProxyService();
$generator->generate([__DIR__], $this->tempOutputDir);
$generator->generate([__DIR__], [], $this->tempOutputDir);

$generatedFile = $this->tempOutputDir.'/Product.php';
self::assertTrue(file_exists($generatedFile));
Expand All @@ -66,16 +66,45 @@ public function testGenerate()
[T_NS_SEPARATOR],
[T_STRING, 'Service'],
[T_NS_SEPARATOR],
[T_STRING, 'ProxyGeneratorTest_ProductTrait'],
[T_STRING, 'EntityProxyServiceTest_ProductTrait'],
]);
self::assertNotNull($sequence);
}

public function testGenerateExcluded()
{
$generator = new EntityProxyService();
$generator->generate([__DIR__], [], $this->tempOutputDir);

$generatedFile = $this->tempOutputDir.'/Product.php';
self::assertTrue(file_exists($generatedFile));

$tokens = Tokens::fromCode(file_get_contents($generatedFile));
$traitTokens = [
[CT::T_USE_TRAIT],
[T_NS_SEPARATOR],
[T_STRING, 'Eccube'],
[T_NS_SEPARATOR],
[T_STRING, 'Tests'],
[T_NS_SEPARATOR],
[T_STRING, 'Service'],
[T_NS_SEPARATOR],
[T_STRING, 'EntityProxyServiceTest_ProductTrait'],
];

self::assertNotNull($tokens->findSequence($traitTokens), 'Traitはあるはず');

// 除外して生成
$generator->generate([], [__DIR__], $this->tempOutputDir);
$tokens = Tokens::fromCode(file_get_contents($generatedFile));
self::assertNull($tokens->findSequence($traitTokens), 'Traitが外されているはず');
}
}

/**
* @EntityExtension("Eccube\Entity\Product")
*/
trait ProxyGeneratorTest_ProductTrait
trait EntityProxyServiceTest_ProductTrait
{
public $testProperty;
}
Loading