Skip to content

Commit

Permalink
Merge pull request #2531 from kiy0taka/dev/proxy-gen
Browse files Browse the repository at this point in the history
プラグインのインストールに合わせてEntity拡張機能によるスキーマ更新を行う対応
  • Loading branch information
Chihiro Adachi authored Oct 4, 2017
2 parents 477c219 + 17a51a1 commit f1298ff
Show file tree
Hide file tree
Showing 11 changed files with 777 additions and 168 deletions.
7 changes: 0 additions & 7 deletions app/Acme/Entity/BaseInfoTrait.php
Original file line number Diff line number Diff line change
Expand Up @@ -24,11 +24,4 @@ trait BaseInfoTrait
* })
*/
public $company_name_vn;

/**
* @ORM\ManyToOne(targetEntity="\Eccube\Entity\Master\Db")
* @ORM\JoinColumn(name="database_id", referencedColumnName="id")
* @Eccube\FormAppend()
*/
public $DataBase;
}
123 changes: 15 additions & 108 deletions src/Eccube/Command/GenerateProxyCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -23,13 +23,12 @@

namespace Eccube\Command;

use Doctrine\Common\Annotations\AnnotationReader;
use Eccube\Annotation\EntityExtension;
use Eccube\Entity\ProxyGenerator;
use Eccube\Repository\PluginRepository;
use Eccube\Service\EntityProxyService;
use Knp\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Filesystem\Filesystem;
use Symfony\Component\Finder\Finder;


class GenerateProxyCommand extends Command
Expand All @@ -45,109 +44,17 @@ protected function execute(InputInterface $input, OutputInterface $output)
{
/** @var \Eccube\Application $app */
$app = $this->getSilexApplication();

// // プロキシのクリア
// $files = Finder::create()
// ->in($app['config']['root_dir'].'/app/cache/doctrine/entity-proxies')
// ->name('*.php')
// ->files();
// $fs = new Filesystem();
// foreach ($files as $file) {
// $output->writeln('remove -> '.$file->getRealPath());
// $fs->remove($file->getRealPath());
// }

// Acmeからファイルを抽出
$files = Finder::create()
->in(
[
$app['config']['root_dir'].'/app/Acme/Entity',
$app['config']['root_dir'].'/app/Plugin/*/Entity',
]
)
->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;
}
}
// プロキシファイルの生成
foreach ($proxies as $targetEntity => $traits) {
$rc = new \Zend\Code\Reflection\ClassReflection($targetEntity);
$generator
= \Zend\Code\Generator\ClassGenerator::fromReflection($rc);

$uses = \Zend\Code\Generator\FileGenerator::fromReflectedFileName($rc->getFileName())
->getUses();

foreach ($uses as $use) {
$generator->addUse($use[0], $use[1]);
}

foreach ($traits as $trait) {
$rt = new \Zend\Code\Reflection\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());
}
}
$generator->addTrait('\\'.$trait);
}

// extendしたクラスが相対パスになるので
$extendClass = $generator->getExtendedClass();
$generator->setExtendedClass('\\'.$extendClass);

// interfaceが相対パスになるので
$interfaces = $generator->getImplementedInterfaces();
foreach ($interfaces as &$interface) {
$interface = '\\'.$interface;
}
$generator->setImplementedInterfaces($interfaces);

$dir = $app['config']['root_dir'].'/app/proxy/entity';
$file = basename($rc->getFileName());

$code = $generator->generate();
file_put_contents($dir.'/'.$file, '<?php '.PHP_EOL.$code);
$output->writeln('gen -> '.$dir.'/'.$file);
}
/** @var EntityProxyService $entityProxyService */
$entityProxyService = $app[EntityProxyService::class];

$dirs = array_map(function($p) use ($app) {
return $app['config']['root_dir'].'/app/Plugin/'.$p->getCode().'/Entity';
}, $app[PluginRepository::class]->findAllEnabled());

$entityProxyService->generate(
array_merge([$app['config']['root_dir'].'/app/Acme/Entity'], $dirs),
$app['config']['root_dir'].'/app/proxy/entity',
$output
);
}
}
2 changes: 1 addition & 1 deletion src/Eccube/Command/PluginCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ protected function configure()

protected function getPluginFromCode($pluginCode)
{
return $this->app['eccube.repository.plugin']->findOneBy(array('del_flg' => 0, 'code' => $pluginCode));
return $this->app['eccube.repository.plugin']->findOneBy(array('code' => $pluginCode));
}

protected function execute(InputInterface $input, OutputInterface $output)
Expand Down
3 changes: 2 additions & 1 deletion src/Eccube/Doctrine/EventSubscriber/LoadEventSubscriber.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

namespace Eccube\Doctrine\EventSubscriber;

use Doctrine\Common\Annotations\AnnotationReader;
use Doctrine\Common\EventSubscriber;
use Doctrine\Common\Persistence\Event\LifecycleEventArgs;
use Doctrine\ORM\Events;
Expand Down Expand Up @@ -32,7 +33,7 @@ public function postLoad(LifecycleEventArgs $args)
{
$entity = $args->getObject();
if ($entity instanceof AbstractEntity) {
$entity->setAnnotationReader($this->app['eccube.di.annotation_reader']);
$entity->setAnnotationReader(isset($this->app['eccube.di.annotation_reader']) ? $this->app['eccube.di.annotation_reader'] : new AnnotationReader());
}
}
}
155 changes: 155 additions & 0 deletions src/Eccube/Doctrine/ORM/Mapping/Driver/ReloadSafeAnnotationDriver.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
<?php
/*
* This file is part of EC-CUBE
*
* Copyright(c) 2000-2017 LOCKON CO.,LTD. All Rights Reserved.
*
* http://www.lockon.co.jp/
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation; either version 2
* of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
*/

namespace Eccube\Doctrine\ORM\Mapping\Driver;



use Doctrine\ORM\Mapping\MappingException;
use Eccube\Util\Str;
use PhpCsFixer\Tokenizer\Token;
use PhpCsFixer\Tokenizer\Tokens;

/**
* 同じプロセス内で新しく生成されたProxyクラスからマッピングメタデータを抽出するためのAnnotationDriver.
*
* 同じプロセス内で、Proxy元のEntityがロードされた後に同じFQCNを持つProxyをロードしようとすると、Fatalエラーが発生する.
* このエラーを回避するために、新しく生成されたProxyクラスは一時的にクラス名を変更してからロードして、マッピングメタデータを抽出する.
*/
class ReloadSafeAnnotationDriver extends AnnotationDriver
{
/**
* @var array 新しく生成されたProxyファイルのリスト
*/
protected $newProxyFiles;

protected $outputDir;

public function setNewProxyFiles($newProxyFiles)
{
$this->newProxyFiles = $newProxyFiles;
}

/**
* @param string $outputDir
*/
public function setOutputDir($outputDir)
{
$this->outputDir = $outputDir;
}

/**
* {@inheritDoc}
*/
public function getAllClassNames()
{
if ($this->classNames !== null) {
return $this->classNames;
}

if (!$this->paths) {
throw MappingException::pathRequired();
}

foreach ($this->paths as $path) {
if ( ! is_dir($path)) {
throw MappingException::fileMappingDriversRequireConfiguredDirectoryPath($path);
}

$iterator = new \RegexIterator(
new \RecursiveIteratorIterator(
new \RecursiveDirectoryIterator($path, \FilesystemIterator::SKIP_DOTS),
\RecursiveIteratorIterator::LEAVES_ONLY
),
'/^.+' . preg_quote($this->fileExtension) . '$/i',
\RecursiveRegexIterator::GET_MATCH
);

foreach ($iterator as $file) {
$sourceFile = $file[0];

if ( ! preg_match('(^phar:)i', $sourceFile)) {
$sourceFile = realpath($sourceFile);
}

foreach ($this->excludePaths as $excludePath) {
$exclude = str_replace('\\', '/', realpath($excludePath));
$current = str_replace('\\', '/', $sourceFile);

if (strpos($current, $exclude) !== false) {
continue 2;
}
}

$proxyFile = str_replace($path, $this->trait_proxies_directory, $sourceFile);
if (file_exists($proxyFile)) {
$sourceFile = $proxyFile;
}

$this->classNames = array_merge($this->classNames ?: [], $this->getClassNamesFromTokens($sourceFile));
}
}

return $this->classNames;
}

/**
* ソースコードを字句解析してクラス名を解決します.
* 新しく生成されたProxyクラスの場合は、一時的にクラス名を変更したクラスを生成してロードします.
*
* @param $sourceFile string ソースファイル
* @return array ソースファイルに含まれるクラス名のリスト
*/
private function getClassNamesFromTokens($sourceFile)
{
$tokens = Tokens::fromCode(file_get_contents($sourceFile));
$results = [];
$currentIndex = 0;
while ($currentIndex = $tokens->getNextTokenOfKind($currentIndex, [[T_CLASS]])) {
$classNameTokenIndex = $tokens->getNextMeaningfulToken($currentIndex);
if ($classNameTokenIndex) {
$namespaceIndex = $tokens->getNextTokenOfKind(0, [[T_NAMESPACE]]);
if ($namespaceIndex) {
$namespaceEndIndex = $tokens->getNextTokenOfKind($namespaceIndex, [';']);
$namespace = $tokens->generatePartialCode($tokens->getNextMeaningfulToken($namespaceIndex), $tokens->getPrevMeaningfulToken($namespaceEndIndex));
$className = $tokens[$classNameTokenIndex]->getContent();
$fqcn = $namespace . '\\' . $className;
if (class_exists($fqcn) && ! $this->isTransient($fqcn)) {
if (in_array($sourceFile, $this->newProxyFiles)) {
$newClassName = $className . Str::random(12);
$tokens[$classNameTokenIndex] = new Token([T_STRING, $newClassName]);
$newFilePath = $this->outputDir."${newClassName}.php";
file_put_contents($newFilePath, $tokens->generateCode());
require_once $newFilePath;
$results[] = $namespace . "\\${newClassName}";
} else {
$results[] = $fqcn;
}
}
}
}
$currentIndex++;
}
return $results;
}
}
9 changes: 4 additions & 5 deletions src/Eccube/Repository/TaxRuleRepository.php
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,6 @@
use Eccube\Annotation\Inject;
use Eccube\Annotation\Repository;
use Eccube\Application;
use Eccube\Entity\BaseInfo;
use Eccube\Common\Constant;
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorage;
use Symfony\Component\Security\Core\Authorization\AuthorizationChecker;
Expand All @@ -50,10 +49,10 @@ class TaxRuleRepository extends AbstractRepository
protected $appConfig;

/**
* @Inject(BaseInfo::class)
* @var BaseInfo
* @Inject(BaseInfoRepository::class)
* @var BaseInfoRepository
*/
protected $BaseInfo;
protected $baseInfoRepository;

/**
* @Inject("security.authorization_checker")
Expand Down Expand Up @@ -109,7 +108,7 @@ public function getByRule($Product = null, $ProductClass = null, $Pref = null, $
}

// 商品単位税率設定がOFFの場合
if ($this->BaseInfo->getOptionProductTaxRule() !== Constant::ENABLED) {
if ($this->baseInfoRepository->get()->getOptionProductTaxRule() !== Constant::ENABLED) {
$Product = null;
$ProductClass = null;
}
Expand Down
Loading

0 comments on commit f1298ff

Please sign in to comment.