diff --git a/app/etc/di.xml b/app/etc/di.xml index f0f27ea0e7d83..bf45058261fd2 100644 --- a/app/etc/di.xml +++ b/app/etc/di.xml @@ -788,7 +788,7 @@ \Magento\Framework\Api\Code\Generator\ExtensionAttributesInterfaceFactoryGenerator \Magento\Framework\ObjectManager\Code\Generator\Factory \Magento\Framework\ObjectManager\Code\Generator\Proxy - \Magento\Framework\Interception\Code\Generator\Interceptor + Magento\Framework\Interception\Code\Generator\InterceptorInterface \Magento\Framework\ObjectManager\Profiler\Code\Generator\Logger \Magento\Framework\Api\Code\Generator\Mapper \Magento\Framework\ObjectManager\Code\Generator\Persistor @@ -802,6 +802,21 @@ + + + \Magento\Framework\CompiledInterception\Generator\FileCache + compiled_plugins + \Magento\Framework\CompiledInterception\Generator\NoSerialize + + + + + \Magento\Framework\CompiledInterception\PluginList\PluginList + + + + @@ -1825,9 +1840,6 @@ type="Magento\Framework\Mail\MimeMessage" /> - - - diff --git a/dev/tests/integration/framework/Magento/TestFramework/Application.php b/dev/tests/integration/framework/Magento/TestFramework/Application.php index 43bb7852e2441..14dcf4dd4b9a6 100644 --- a/dev/tests/integration/framework/Magento/TestFramework/Application.php +++ b/dev/tests/integration/framework/Magento/TestFramework/Application.php @@ -20,6 +20,7 @@ * * @SuppressWarnings(PHPMD.CouplingBetweenObjects) * @SuppressWarnings(PHPMD.TooManyFields) + * phpcs:disable Magento2.Functions.DiscouragedFunction */ class Application { @@ -725,6 +726,7 @@ protected function getCustomDirs() DirectoryList::STATIC_VIEW => [$path => "{$this->installDir}/pub/static"], DirectoryList::TMP_MATERIALIZATION_DIR => [$path => "{$var}/view_preprocessed/pub/static"], DirectoryList::GENERATED_CODE => [$path => "{$generated}/code"], + DirectoryList::STATIC_CACHE => [$path => "{$generated}/static_cache"], DirectoryList::CACHE => [$path => "{$var}/cache"], DirectoryList::LOG => [$path => "{$var}/log"], DirectoryList::SESSION => [$path => "{$var}/session"], diff --git a/dev/tests/integration/framework/Magento/TestFramework/Interception/CompiledPluginList.php b/dev/tests/integration/framework/Magento/TestFramework/Interception/CompiledPluginList.php new file mode 100644 index 0000000000000..178e58fcb4f77 --- /dev/null +++ b/dev/tests/integration/framework/Magento/TestFramework/Interception/CompiledPluginList.php @@ -0,0 +1,66 @@ +pluginList = $objectManager->create( + PluginList::class, + [ + $objectManager->get(Dom::class), + $objectManager->get(ScopeInterface ::class), + $objectManager->get(FileCache ::class), + $objectManager->get(ObjectManagerRelationsRuntime ::class), + $objectManager->get(ConfigInterface::class), + $objectManager->get(InterceptionDefinitionRuntime ::class), + $objectManager, + $objectManager->get(ObjectManagerDefinitionRuntime ::class), + ['global'], + 'compiled_plugins', + $objectManager->get(NoSerialize ::class), + ] + ); + parent::__construct($this->pluginList); + } + + /** + * Reset internal cache + */ + public function reset() + { + $this->pluginList->reset(); + } +} diff --git a/dev/tests/integration/testsuite/Magento/Framework/CompiledInterception/CompiledInterceptor/CompiledInterceptorTest.php b/dev/tests/integration/testsuite/Magento/Framework/CompiledInterception/CompiledInterceptor/CompiledInterceptorTest.php new file mode 100644 index 0000000000000..c18f92b0d3afc --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Framework/CompiledInterception/CompiledInterceptor/CompiledInterceptorTest.php @@ -0,0 +1,182 @@ +ioGenerator = $this->getMockBuilder(Io::class) + ->disableOriginalConstructor() + ->getMock(); + + $this->areaList = $this->getMockBuilder(AreaList::class) + ->disableOriginalConstructor() + ->getMock(); + } + + /** + * @return array + */ + public function createScopeReaders() + { + $readerMap = include __DIR__ . '/_files/reader_mock_map.php'; + $readerMock = $this->createMock(Dom::class); + $readerMock->method('read')->willReturnMap($readerMap); + + $omMock = $this->createMock(ObjectManager::class); + + $omConfigMock = $this->getMockForAbstractClass( + ConfigInterface::class + ); + + $omConfigMock->method('getOriginalInstanceType')->willReturnArgument(0); + $ret = []; + $objectManagerHelper = new ObjectManagerHelper($this); + $directoryList = ObjectManager::getInstance()->get(DirectoryList::class); + //clear static cache + $fileCache = new FileCache($directoryList); + $fileCache->clean(); + foreach ($readerMap as $readerLine) { + $pluginList = ObjectManager::getInstance()->create( + PluginList::class, + [ + 'objectManager' => $omMock, + 'configScope' => new StaticScope($readerLine[0]), + 'reader' => $readerMock, + 'omConfig' => $omConfigMock, + 'cache' => $fileCache, + 'cachePath' => false, + 'serializer' => new NoSerialize() + ] + ); + + $ret[$readerLine[0]] = $objectManagerHelper->getObject( + CompiledPluginList::class, + [ + 'pluginList' => $pluginList + ] + ); + } + return $ret; + } + + /** + * Checks a test case when interceptor generates code for the specified class. + * + * @param string $className + * @param string $resultClassName + * @param string $fileName + * @dataProvider interceptorDataProvider + */ + public function testGenerate($className, $resultClassName, $fileName) + { + $objectManagerHelper = new ObjectManagerHelper($this); + /** @var AreasPluginList $areaPlugins */ + $areaPlugins = $objectManagerHelper->getObject( + AreasPluginList::class, + [ + 'areaList' => $this->areaList, + 'scopeInterfaceFactory' => $objectManagerHelper->getObject(ScopeInterfaceFactory::class), + 'compiledPluginListFactory' => $objectManagerHelper->getObject(CompiledPluginListFactory::class), + 'plugins' => $this->createScopeReaders() + ] + ); + + /** @var CompiledInterceptor|MockObject $interceptor */ + $interceptor = $this->getMockBuilder(CompiledInterceptor::class) + ->setMethods(['_validateData']) + ->setConstructorArgs( + [ + $areaPlugins, + $className, + $resultClassName, + $this->ioGenerator, + null, + null + ] + ) + ->getMock(); + + $this->ioGenerator->method('generateResultFileName')->with('\\' . $resultClassName) + ->willReturn($fileName . '.php'); + + $code = file_get_contents(__DIR__ . '/_out_interceptors/' . $fileName . '.txt'); + + $this->ioGenerator->method('writeResultFile')->with($fileName . '.php', $code); + $interceptor->method('_validateData')->willReturn(true); + + $generated = $interceptor->generate(); + $this->assertEquals($fileName . '.php', $generated, 'Generated interceptor is invalid.'); + } + + /** + * Gets list of interceptor samples. + * + * @return array + */ + public function interceptorDataProvider() + { + return [ + [ + Item::class, + Item::class . '\Interceptor', + 'Item' + ], + [ + ComplexItem::class, + ComplexItem::class . '\Interceptor', + 'ComplexItem' + ], + [ + ComplexItemTyped::class, + ComplexItemTyped::class . '\Interceptor', + 'ComplexItemTyped' + ], + [ + SecondItem::class, + SecondItem::class . '\Interceptor', + 'SecondItem' + ], + ]; + } +} diff --git a/dev/tests/integration/testsuite/Magento/Framework/CompiledInterception/CompiledInterceptor/Custom/Module/Model/ComplexItem.php b/dev/tests/integration/testsuite/Magento/Framework/CompiledInterception/CompiledInterceptor/Custom/Module/Model/ComplexItem.php new file mode 100644 index 0000000000000..313ca3462a41b --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Framework/CompiledInterception/CompiledInterceptor/Custom/Module/Model/ComplexItem.php @@ -0,0 +1,65 @@ +attribute; + } + + /** + * @param $value + */ + public function setValue($value) + { + $this->attribute = $value; + } + + public function & getReference() + { + } + + /** + * @param mixed ...$variadicValue + */ + public function firstVariadicParameter(...$variadicValue) + { + $this->variadicAttribute = $variadicValue; + } + + /** + * @param $value + * @param mixed ...$variadicValue + */ + public function secondVariadicParameter($value, ...$variadicValue) + { + $this->attribute = $value; + $this->variadicAttribute = $variadicValue; + } + + /** + * @param mixed ...$variadicValue + */ + public function byRefVariadic(& ...$variadicValue) + { + $this->variadicAttribute = $variadicValue; + } + + /** + * + */ + public function returnsSelf() + { + return $this; + } +} diff --git a/dev/tests/integration/testsuite/Magento/Framework/CompiledInterception/CompiledInterceptor/Custom/Module/Model/ComplexItemTyped.php b/dev/tests/integration/testsuite/Magento/Framework/CompiledInterception/CompiledInterceptor/Custom/Module/Model/ComplexItemTyped.php new file mode 100644 index 0000000000000..35874d688fe31 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Framework/CompiledInterception/CompiledInterceptor/Custom/Module/Model/ComplexItemTyped.php @@ -0,0 +1,85 @@ +value; + } + + /** + * @param string $value + */ + public function setValue(string $value) + { + $this->value = $value; + } + + /** + * @param string ...$variadicValue + */ + public function firstVariadicParameter(string ...$variadicValue) + { + $this->variadicValue = $variadicValue; + } + + /** + * @param string $value + * @param string ...$variadicValue + */ + public function secondVariadicParameter(string $value, string ...$variadicValue) + { + $this->value = $value; + $this->variadicValue = $variadicValue; + } + + /** + * @param string ...$variadicValue + */ + public function byRefVariadic(string & ...$variadicValue) + { + $this->variadicValue = $variadicValue; + } + + /** + * + */ + public function returnsSelf(): self + { + return $this; + } + + /** + * + */ + public function returnsType(): \Magento\Framework\Something + { + return null; + } +} diff --git a/dev/tests/integration/testsuite/Magento/Framework/CompiledInterception/CompiledInterceptor/Custom/Module/Model/Item.php b/dev/tests/integration/testsuite/Magento/Framework/CompiledInterception/CompiledInterceptor/Custom/Module/Model/Item.php new file mode 100644 index 0000000000000..71b11fce7c714 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Framework/CompiledInterception/CompiledInterceptor/Custom/Module/Model/Item.php @@ -0,0 +1,19 @@ + [ + 'plugins' => [ + 'simple_plugin' => [ + 'sortOrder' => 10, + 'instance' => Simple::class, + ], + ], + ], + ComplexItem::class => [ + 'plugins' => [ + 'advanced_plugin' => [ + 'sortOrder' => 5, + 'instance' => Advanced::class, + ], + ], + ], + ], + ], + [ + 'backend', + [ + Item::class => [ + 'plugins' => [ + 'advanced_plugin' => [ + 'sortOrder' => 5, + 'instance' => Advanced::class, + ], + ], + ], + ComplexItem::class => [ + 'plugins' => [ + 'complex_plugin' => [ + 'sortOrder' => 15, + 'instance' => Complex::class, + ], + 'advanced_plugin' => [ + 'sortOrder' => 5, + 'instance' => Advanced::class, + ], + ], + ], + ComplexItemTyped::class => [ + 'plugins' => [ + 'complex_plugin' => [ + 'sortOrder' => 25, + 'instance' => Complex::class, + ], + 'advanced_plugin' => [ + 'sortOrder' => 5, + 'instance' => Advanced::class, + ], + ], + ], + ] + ], + [ + 'frontend', + [ + Item::class => [ + 'plugins' => ['simple_plugin' => ['disabled' => true]], + ], + Enhanced::class => [ + 'plugins' => [ + 'advanced_plugin' => [ + 'sortOrder' => 5, + 'instance' => Advanced::class, + ], + ], + ], + 'SomeType' => [ + 'plugins' => [ + 'simple_plugin' => [ + 'instance' => 'NonExistingPluginClass', + ], + ], + ], + 'typeWithoutInstance' => [ + 'plugins' => [ + 'simple_plugin' => [], + ], + ], + SecondItem::class => [ + 'plugins' => [ + 'simple_plugin1' => [ + 'sortOrder' => 5, + 'instance' => Simple::class, + ], + 'advanced_plugin1' => [ + 'sortOrder' => 5, + 'instance' => Advanced::class, + ], + 'advanced_plugin2' => [ + 'sortOrder' => 10, + 'instance' => Advanced::class, + ], + 'simple_plugin2' => [ + 'sortOrder' => 11, + 'instance' => Simple::class, + ], + 'simple_plugin3' => [ + 'sortOrder' => 12, + 'instance' => Simple::class, + ], + 'advanced_plugin3' => [ + 'sortOrder' => 15, + 'instance' => Advanced::class, + ], + 'advanced_plugin4' => [ + 'sortOrder' => 25, + 'instance' => Advanced::class, + ], + ], + ] + ] + ], + [ + 'emptyscope', + [ + + ] + ] +]; diff --git a/dev/tests/integration/testsuite/Magento/Framework/CompiledInterception/CompiledInterceptor/_out_interceptors/ComplexItem.txt b/dev/tests/integration/testsuite/Magento/Framework/CompiledInterception/CompiledInterceptor/_out_interceptors/ComplexItem.txt new file mode 100644 index 0000000000000..dd0c9155e95f4 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Framework/CompiledInterception/CompiledInterceptor/_out_interceptors/ComplexItem.txt @@ -0,0 +1,171 @@ +namespace Magento\Framework\CompiledInterception\CompiledInterceptor\Custom\Module\Model\ComplexItem; + +use Magento\Framework\Config\ScopeInterface; +use Magento\Framework\ObjectManagerInterface; + +/** + * Interceptor class for @see \Magento\Framework\CompiledInterception\CompiledInterceptor\Custom\Module\Model\ComplexItem + */ +class Interceptor extends \Magento\Framework\CompiledInterception\CompiledInterceptor\Custom\Module\Model\ComplexItem +{ + /** + * @var ScopeInterface + */ + private $____scope = null; + + /** + * @var ObjectManagerInterface + */ + private $____om = null; + + /** + * @inheritdoc + */ + public function __construct() + { + $this->____om = \Magento\Framework\App\ObjectManager::getInstance(); + $this->____scope = \Magento\Framework\App\ObjectManager::getInstance()->get(\Magento\Framework\Config\ScopeInterface::class); + } + + /** + * @inheritdoc + */ + public function getName() + { + switch ($this->____scope->getCurrentScope()) { + case 'global': + case 'frontend': + case 'emptyscope': + $this->____plugin_advanced_plugin()->beforeGetName($this); + + $result = $this->____plugin_advanced_plugin()->aroundGetName($this, function(){ + return parent::getName(); + }); + + return $this->____plugin_advanced_plugin()->afterGetName($this, $result); + default: + $this->____plugin_advanced_plugin()->beforeGetName($this); + + $result = $this->____plugin_advanced_plugin()->aroundGetName($this, function(){ + $result = $this->____plugin_complex_plugin()->aroundGetName($this, function(){ + return parent::getName(); + }); + + return $this->____plugin_complex_plugin()->afterGetName($this, $result); + }); + + return $this->____plugin_advanced_plugin()->afterGetName($this, $result); + } + } + + /** + * @inheritdoc + */ + public function setValue($value) + { + switch ($this->____scope->getCurrentScope()) { + case 'backend': + $beforeResult = $this->____plugin_complex_plugin()->beforeSetValue($this, $value); + if ($beforeResult !== null) list($value) = (array)$beforeResult; + + return $this->____plugin_complex_plugin()->aroundSetValue($this, function($value){ + return parent::setValue($value); + }, $value); + default: + return parent::setValue($value); + } + } + + /** + * @inheritdoc + */ + public function & getReference() + { + switch ($this->____scope->getCurrentScope()) { + case 'backend': + return $this->____plugin_complex_plugin()->aroundGetReference($this, function(){ + return parent::getReference(); + }); + default: + return parent::getReference(); + } + } + + /** + * @inheritdoc + */ + public function firstVariadicParameter(... $variadicValue) + { + switch ($this->____scope->getCurrentScope()) { + case 'backend': + return $this->____plugin_complex_plugin()->aroundFirstVariadicParameter($this, function($variadicValue){ + return parent::firstVariadicParameter($variadicValue); + }, $variadicValue); + default: + return parent::firstVariadicParameter($variadicValue); + } + } + + /** + * @inheritdoc + */ + public function secondVariadicParameter($value, ... $variadicValue) + { + switch ($this->____scope->getCurrentScope()) { + case 'backend': + return $this->____plugin_complex_plugin()->aroundSecondVariadicParameter($this, function($value, $variadicValue){ + return parent::secondVariadicParameter($value, $variadicValue); + }, $value, $variadicValue); + default: + return parent::secondVariadicParameter($value, $variadicValue); + } + } + + /** + * @inheritdoc + */ + public function byRefVariadic(&... $variadicValue) + { + switch ($this->____scope->getCurrentScope()) { + case 'backend': + return $this->____plugin_complex_plugin()->aroundByRefVariadic($this, function(&$variadicValue){ + return parent::byRefVariadic($variadicValue); + }, $variadicValue); + default: + return parent::byRefVariadic($variadicValue); + } + } + + /** + * @inheritdoc + */ + public function returnsSelf() + { + switch ($this->____scope->getCurrentScope()) { + case 'backend': + $this->____plugin_complex_plugin()->beforeReturnsSelf($this); + + return parent::returnsSelf(); + default: + return parent::returnsSelf(); + } + } + + /** + * plugin "advanced_plugin" + * @return \Magento\Framework\CompiledInterception\CompiledInterceptor\Custom\Module\Model\ItemPlugin\Advanced + */ + private function ____plugin_advanced_plugin() + { + return $this->____om->get(\Magento\Framework\CompiledInterception\CompiledInterceptor\Custom\Module\Model\ItemPlugin\Advanced::class); + } + + /** + * plugin "complex_plugin" + * @return \Magento\Framework\CompiledInterception\CompiledInterceptor\Custom\Module\Model\ItemPlugin\Complex + */ + private function ____plugin_complex_plugin() + { + return $this->____om->get(\Magento\Framework\CompiledInterception\CompiledInterceptor\Custom\Module\Model\ItemPlugin\Complex::class); + } +} diff --git a/dev/tests/integration/testsuite/Magento/Framework/CompiledInterception/CompiledInterceptor/_out_interceptors/ComplexItemTyped.txt b/dev/tests/integration/testsuite/Magento/Framework/CompiledInterception/CompiledInterceptor/_out_interceptors/ComplexItemTyped.txt new file mode 100644 index 0000000000000..2ba7c318be025 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Framework/CompiledInterception/CompiledInterceptor/_out_interceptors/ComplexItemTyped.txt @@ -0,0 +1,195 @@ +namespace Magento\Framework\CompiledInterception\CompiledInterceptor\Custom\Module\Model\ComplexItemTyped; + +use Magento\Framework\Config\ScopeInterface; +use Magento\Framework\ObjectManagerInterface; + +/** + * Interceptor class for @see \Magento\Framework\CompiledInterception\CompiledInterceptor\Custom\Module\Model\ComplexItemTyped + */ +class Interceptor extends \Magento\Framework\CompiledInterception\CompiledInterceptor\Custom\Module\Model\ComplexItemTyped +{ + /** + * @var ScopeInterface + */ + private $____scope = null; + + /** + * @var ObjectManagerInterface + */ + private $____om = null; + + /** + * @inheritdoc + */ + public function __construct() + { + $this->____om = \Magento\Framework\App\ObjectManager::getInstance(); + $this->____scope = \Magento\Framework\App\ObjectManager::getInstance()->get(\Magento\Framework\Config\ScopeInterface::class); + } + + /** + * @inheritdoc + */ + public function returnVoid() : void + { + switch ($this->____scope->getCurrentScope()) { + case 'backend': + parent::returnVoid(); + + $this->____plugin_complex_plugin()->afterReturnVoid($this, null); + break; + default: + parent::returnVoid(); + break; + } + } + + /** + * @inheritdoc + */ + public function getNullableValue() : ?string + { + switch ($this->____scope->getCurrentScope()) { + case 'backend': + $result = parent::getNullableValue(); + + return $this->____plugin_complex_plugin()->afterGetNullableValue($this, $result); + default: + return parent::getNullableValue(); + } + } + + /** + * @inheritdoc + */ + public function getName() : string + { + switch ($this->____scope->getCurrentScope()) { + case 'backend': + $this->____plugin_advanced_plugin()->beforeGetName($this); + + $result = $this->____plugin_advanced_plugin()->aroundGetName($this, function(){ + $result = $this->____plugin_complex_plugin()->aroundGetName($this, function(){ + return parent::getName(); + }); + + return $this->____plugin_complex_plugin()->afterGetName($this, $result); + }); + + return $this->____plugin_advanced_plugin()->afterGetName($this, $result); + default: + return parent::getName(); + } + } + + /** + * @inheritdoc + */ + public function setValue(string $value) + { + switch ($this->____scope->getCurrentScope()) { + case 'backend': + $beforeResult = $this->____plugin_complex_plugin()->beforeSetValue($this, $value); + if ($beforeResult !== null) list($value) = (array)$beforeResult; + + return $this->____plugin_complex_plugin()->aroundSetValue($this, function($value){ + return parent::setValue($value); + }, $value); + default: + return parent::setValue($value); + } + } + + /** + * @inheritdoc + */ + public function firstVariadicParameter(string ... $variadicValue) + { + switch ($this->____scope->getCurrentScope()) { + case 'backend': + return $this->____plugin_complex_plugin()->aroundFirstVariadicParameter($this, function($variadicValue){ + return parent::firstVariadicParameter($variadicValue); + }, $variadicValue); + default: + return parent::firstVariadicParameter($variadicValue); + } + } + + /** + * @inheritdoc + */ + public function secondVariadicParameter(string $value, string ... $variadicValue) + { + switch ($this->____scope->getCurrentScope()) { + case 'backend': + return $this->____plugin_complex_plugin()->aroundSecondVariadicParameter($this, function($value, $variadicValue){ + return parent::secondVariadicParameter($value, $variadicValue); + }, $value, $variadicValue); + default: + return parent::secondVariadicParameter($value, $variadicValue); + } + } + + /** + * @inheritdoc + */ + public function byRefVariadic(string &... $variadicValue) + { + switch ($this->____scope->getCurrentScope()) { + case 'backend': + return $this->____plugin_complex_plugin()->aroundByRefVariadic($this, function(&$variadicValue){ + return parent::byRefVariadic($variadicValue); + }, $variadicValue); + default: + return parent::byRefVariadic($variadicValue); + } + } + + /** + * @inheritdoc + */ + public function returnsSelf() : \Magento\Framework\CompiledInterception\CompiledInterceptor\Custom\Module\Model\ComplexItemTyped + { + switch ($this->____scope->getCurrentScope()) { + case 'backend': + $this->____plugin_complex_plugin()->beforeReturnsSelf($this); + + return parent::returnsSelf(); + default: + return parent::returnsSelf(); + } + } + + /** + * @inheritdoc + */ + public function returnsType() : \Magento\Framework\Something + { + switch ($this->____scope->getCurrentScope()) { + case 'backend': + $this->____plugin_complex_plugin()->beforeReturnsType($this); + + return parent::returnsType(); + default: + return parent::returnsType(); + } + } + + /** + * plugin "complex_plugin" + * @return \Magento\Framework\CompiledInterception\CompiledInterceptor\Custom\Module\Model\ItemPlugin\Complex + */ + private function ____plugin_complex_plugin() + { + return $this->____om->get(\Magento\Framework\CompiledInterception\CompiledInterceptor\Custom\Module\Model\ItemPlugin\Complex::class); + } + + /** + * plugin "advanced_plugin" + * @return \Magento\Framework\CompiledInterception\CompiledInterceptor\Custom\Module\Model\ItemPlugin\Advanced + */ + private function ____plugin_advanced_plugin() + { + return $this->____om->get(\Magento\Framework\CompiledInterception\CompiledInterceptor\Custom\Module\Model\ItemPlugin\Advanced::class); + } +} diff --git a/dev/tests/integration/testsuite/Magento/Framework/CompiledInterception/CompiledInterceptor/_out_interceptors/Item.txt b/dev/tests/integration/testsuite/Magento/Framework/CompiledInterception/CompiledInterceptor/_out_interceptors/Item.txt new file mode 100644 index 0000000000000..b14d36d5f0e23 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Framework/CompiledInterception/CompiledInterceptor/_out_interceptors/Item.txt @@ -0,0 +1,71 @@ +namespace Magento\Framework\CompiledInterception\CompiledInterceptor\Custom\Module\Model\Item; + +use Magento\Framework\Config\ScopeInterface; +use Magento\Framework\ObjectManagerInterface; + +/** + * Interceptor class for @see \Magento\Framework\CompiledInterception\CompiledInterceptor\Custom\Module\Model\Item + */ +class Interceptor extends \Magento\Framework\CompiledInterception\CompiledInterceptor\Custom\Module\Model\Item +{ + /** + * @var ScopeInterface + */ + private $____scope = null; + + /** + * @var ObjectManagerInterface + */ + private $____om = null; + + /** + * @inheritdoc + */ + public function __construct() + { + $this->____om = \Magento\Framework\App\ObjectManager::getInstance(); + $this->____scope = \Magento\Framework\App\ObjectManager::getInstance()->get(\Magento\Framework\Config\ScopeInterface::class); + } + + /** + * @inheritdoc + */ + public function getName() + { + switch ($this->____scope->getCurrentScope()) { + case 'global': + case 'emptyscope': + $result = parent::getName(); + + return $this->____plugin_simple_plugin()->afterGetName($this, $result); + default: + $this->____plugin_advanced_plugin()->beforeGetName($this); + + $result = $this->____plugin_advanced_plugin()->aroundGetName($this, function(){ + $result = parent::getName(); + + return $this->____plugin_simple_plugin()->afterGetName($this, $result); + }); + + return $this->____plugin_advanced_plugin()->afterGetName($this, $result); + } + } + + /** + * plugin "simple_plugin" + * @return \Magento\Framework\CompiledInterception\CompiledInterceptor\Custom\Module\Model\ItemPlugin\Simple + */ + private function ____plugin_simple_plugin() + { + return $this->____om->get(\Magento\Framework\CompiledInterception\CompiledInterceptor\Custom\Module\Model\ItemPlugin\Simple::class); + } + + /** + * plugin "advanced_plugin" + * @return \Magento\Framework\CompiledInterception\CompiledInterceptor\Custom\Module\Model\ItemPlugin\Advanced + */ + private function ____plugin_advanced_plugin() + { + return $this->____om->get(\Magento\Framework\CompiledInterception\CompiledInterceptor\Custom\Module\Model\ItemPlugin\Advanced::class); + } +} diff --git a/dev/tests/integration/testsuite/Magento/Framework/CompiledInterception/CompiledInterceptor/_out_interceptors/SecondItem.txt b/dev/tests/integration/testsuite/Magento/Framework/CompiledInterception/CompiledInterceptor/_out_interceptors/SecondItem.txt new file mode 100644 index 0000000000000..4df8943d0974a --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Framework/CompiledInterception/CompiledInterceptor/_out_interceptors/SecondItem.txt @@ -0,0 +1,135 @@ +namespace Magento\Framework\CompiledInterception\CompiledInterceptor\Custom\Module\Model\SecondItem; + +use Magento\Framework\Config\ScopeInterface; +use Magento\Framework\ObjectManagerInterface; + +/** + * Interceptor class for @see \Magento\Framework\CompiledInterception\CompiledInterceptor\Custom\Module\Model\SecondItem + */ +class Interceptor extends \Magento\Framework\CompiledInterception\CompiledInterceptor\Custom\Module\Model\SecondItem +{ + /** + * @var ScopeInterface + */ + private $____scope = null; + + /** + * @var ObjectManagerInterface + */ + private $____om = null; + + /** + * @inheritdoc + */ + public function __construct() + { + $this->____om = \Magento\Framework\App\ObjectManager::getInstance(); + $this->____scope = \Magento\Framework\App\ObjectManager::getInstance()->get(\Magento\Framework\Config\ScopeInterface::class); + } + + /** + * @inheritdoc + */ + public function getName() + { + switch ($this->____scope->getCurrentScope()) { + case 'frontend': + $this->____plugin_advanced_plugin1()->beforeGetName($this); + + $result = $this->____plugin_advanced_plugin1()->aroundGetName($this, function(){ + $this->____plugin_advanced_plugin2()->beforeGetName($this); + + $result = $this->____plugin_advanced_plugin2()->aroundGetName($this, function(){ + $this->____plugin_advanced_plugin3()->beforeGetName($this); + + $result = $this->____plugin_advanced_plugin3()->aroundGetName($this, function(){ + $this->____plugin_advanced_plugin4()->beforeGetName($this); + + $result = $this->____plugin_advanced_plugin4()->aroundGetName($this, function(){ + return parent::getName(); + }); + + return $this->____plugin_advanced_plugin4()->afterGetName($this, $result); + }); + + $result = $this->____plugin_simple_plugin2()->afterGetName($this, $result); + + $result = $this->____plugin_simple_plugin3()->afterGetName($this, $result); + + return $this->____plugin_advanced_plugin3()->afterGetName($this, $result); + }); + + return $this->____plugin_advanced_plugin2()->afterGetName($this, $result); + }); + + $result = $this->____plugin_simple_plugin1()->afterGetName($this, $result); + + return $this->____plugin_advanced_plugin1()->afterGetName($this, $result); + default: + return parent::getName(); + } + } + + /** + * plugin "advanced_plugin1" + * @return \Magento\Framework\CompiledInterception\CompiledInterceptor\Custom\Module\Model\ItemPlugin\Advanced + */ + private function ____plugin_advanced_plugin1() + { + return $this->____om->get(\Magento\Framework\CompiledInterception\CompiledInterceptor\Custom\Module\Model\ItemPlugin\Advanced::class); + } + + /** + * plugin "simple_plugin1" + * @return \Magento\Framework\CompiledInterception\CompiledInterceptor\Custom\Module\Model\ItemPlugin\Simple + */ + private function ____plugin_simple_plugin1() + { + return $this->____om->get(\Magento\Framework\CompiledInterception\CompiledInterceptor\Custom\Module\Model\ItemPlugin\Simple::class); + } + + /** + * plugin "advanced_plugin2" + * @return \Magento\Framework\CompiledInterception\CompiledInterceptor\Custom\Module\Model\ItemPlugin\Advanced + */ + private function ____plugin_advanced_plugin2() + { + return $this->____om->get(\Magento\Framework\CompiledInterception\CompiledInterceptor\Custom\Module\Model\ItemPlugin\Advanced::class); + } + + /** + * plugin "advanced_plugin3" + * @return \Magento\Framework\CompiledInterception\CompiledInterceptor\Custom\Module\Model\ItemPlugin\Advanced + */ + private function ____plugin_advanced_plugin3() + { + return $this->____om->get(\Magento\Framework\CompiledInterception\CompiledInterceptor\Custom\Module\Model\ItemPlugin\Advanced::class); + } + + /** + * plugin "simple_plugin2" + * @return \Magento\Framework\CompiledInterception\CompiledInterceptor\Custom\Module\Model\ItemPlugin\Simple + */ + private function ____plugin_simple_plugin2() + { + return $this->____om->get(\Magento\Framework\CompiledInterception\CompiledInterceptor\Custom\Module\Model\ItemPlugin\Simple::class); + } + + /** + * plugin "simple_plugin3" + * @return \Magento\Framework\CompiledInterception\CompiledInterceptor\Custom\Module\Model\ItemPlugin\Simple + */ + private function ____plugin_simple_plugin3() + { + return $this->____om->get(\Magento\Framework\CompiledInterception\CompiledInterceptor\Custom\Module\Model\ItemPlugin\Simple::class); + } + + /** + * plugin "advanced_plugin4" + * @return \Magento\Framework\CompiledInterception\CompiledInterceptor\Custom\Module\Model\ItemPlugin\Advanced + */ + private function ____plugin_advanced_plugin4() + { + return $this->____om->get(\Magento\Framework\CompiledInterception\CompiledInterceptor\Custom\Module\Model\ItemPlugin\Advanced::class); + } +} diff --git a/dev/tests/integration/testsuite/Magento/Framework/CompiledInterception/CompiledPluginList/CompiledPluginListTest.php b/dev/tests/integration/testsuite/Magento/Framework/CompiledInterception/CompiledPluginList/CompiledPluginListTest.php new file mode 100644 index 0000000000000..82367bdd99952 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Framework/CompiledInterception/CompiledPluginList/CompiledPluginListTest.php @@ -0,0 +1,184 @@ +objects = $this->createScopeReaders(); + } + + public function createScopeReaders() + { + $readerMap = include __DIR__ . '/_files/reader_mock_map.php'; + $readerMock = $this->createMock(Dom::class); + $readerMock->method('read')->willReturnMap($readerMap); + + $omMock = $this->createMock(ObjectManager::class); + $omMock->method('get')->with(LoggerInterface::class)->willReturn(new NullLogger()); + + $omConfigMock = $this->getMockForAbstractClass( + ConfigInterface::class + ); + + $omConfigMock->method('getOriginalInstanceType')->willReturnArgument(0); + $ret = []; + $objectManagerHelper = new ObjectManagerHelper($this); + $directoryList = ObjectManager::getInstance()->get(DirectoryList::class); + //clear static cache + $fileCache = new FileCache($directoryList); + $fileCache->clean(); + foreach ($readerMap as $readerLine) { + $pluginList = ObjectManager::getInstance()->create( + PluginList::class, + [ + 'objectManager' => $omMock, + 'configScope' => new StaticScope($readerLine[0]), + 'reader' => $readerMock, + 'omConfig' => $omConfigMock, + 'cache' => $fileCache, + 'cachePath' => false, + 'serializer' => new NoSerialize() + ] + ); + $ret[$readerLine[0]] = $objectManagerHelper->getObject( + CompiledPluginList::class, + [ + 'pluginList' => $pluginList, + ] + ); + } + return $ret; + } + + public function testGetPlugin() + { + $this->objects['backend']->getNext(Item::class, 'getName'); + $this->assertEquals( + Simple::class, + $this->objects['backend']->getPluginType( + Item::class, + 'simple_plugin' + ) + ); + $this->assertEquals( + Advanced::class, + $this->objects['backend']->getPluginType( + Item::class, + 'advanced_plugin' + ) + ); + } + + /** + * @param $expectedResult + * @param $type + * @param $method + * @param $scopeCode + * @param string $code + * @dataProvider getPluginsDataProvider + */ + public function testGetPlugins($expectedResult, $type, $method, $scopeCode, $code = '__self') + { + $this->assertEquals($expectedResult, $this->objects[$scopeCode]->getNext($type, $method, $code)); + } + + /** + * @return array + */ + public function getPluginsDataProvider() + { + return [ + [ + [4 => ['simple_plugin']], Item::class, + 'getName', + 'global', + ], + [ + // advanced plugin has lower sort order + [2 => 'advanced_plugin', 4 => ['advanced_plugin'], 1 => ['advanced_plugin']], + Item::class, + 'getName', + 'backend' + ], + [ + // advanced plugin has lower sort order + [4 => ['simple_plugin']], + Item::class, + 'getName', + 'backend', + 'advanced_plugin' + ], + // simple plugin is disabled in configuration for + // \Magento\Framework\CompiledInterception\CompiledPluginList\Custom\Module\Model\Item + // in frontend + [null, Item::class, 'getName', 'frontend'], + // test plugin inheritance + [ + [4 => ['simple_plugin']], + Enhanced::class, + 'getName', + 'global' + ], + [ + // simple plugin is disabled in configuration for parent + [2 => 'advanced_plugin', 4 => ['advanced_plugin'], 1 => ['advanced_plugin']], + Enhanced::class, + 'getName', + 'frontend' + ] + ]; + } + + /** + * @covers \Magento\Framework\Interception\PluginList\PluginList::getNext + * @covers \Magento\Framework\Interception\PluginList\PluginList::_inheritPlugins + */ + public function testInheritPluginsWithNonExistingClass() + { + $this->objects['frontend']->getNext('SomeType', 'someMethod'); + $this->expectException(InvalidArgumentException::class); + } + + /** + * @covers \Magento\Framework\Interception\PluginList\PluginList::getNext + * @covers \Magento\Framework\Interception\PluginList\PluginList::_inheritPlugins + */ + public function testInheritPluginsWithNotExistingPlugin() + { + $this->assertNull($this->objects['frontend']->getNext('typeWithoutInstance', 'someMethod')); + } +} diff --git a/dev/tests/integration/testsuite/Magento/Framework/CompiledInterception/CompiledPluginList/Custom/Module/Model/Item.php b/dev/tests/integration/testsuite/Magento/Framework/CompiledInterception/CompiledPluginList/Custom/Module/Model/Item.php new file mode 100644 index 0000000000000..2a74773067307 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Framework/CompiledInterception/CompiledPluginList/Custom/Module/Model/Item.php @@ -0,0 +1,22 @@ + [ + 'plugins' => [ + 'simple_plugin' => [ + 'sortOrder' => 10, + 'instance' => Simple::class, + ], + ], + ], + ], + ], + [ + 'backend', + [ + Item::class => [ + 'plugins' => [ + 'advanced_plugin' => [ + 'sortOrder' => 5, + 'instance' => Advanced::class, + ], + ], + ], + ] + ], + [ + 'frontend', + [ + Item::class => [ + 'plugins' => ['simple_plugin' => ['disabled' => true]], + ], + Enhanced::class => [ + 'plugins' => [ + 'advanced_plugin' => [ + 'sortOrder' => 5, + 'instance' => Advanced::class, + ], + ], + ], + 'SomeType' => [ + 'plugins' => [ + 'simple_plugin' => [ + 'instance' => 'NonExistingPluginClass', + ], + ], + ], + 'typeWithoutInstance' => [ + 'plugins' => [ + 'simple_plugin' => [], + ], + ] + ] + ], + [ + 'emptyscope', + [ + + ] + ] +]; diff --git a/dev/tests/integration/testsuite/Magento/Framework/Interception/AbstractPlugin.php b/dev/tests/integration/testsuite/Magento/Framework/Interception/AbstractPlugin.php index b9deeb3bb968f..fad44b5999dab 100644 --- a/dev/tests/integration/testsuite/Magento/Framework/Interception/AbstractPlugin.php +++ b/dev/tests/integration/testsuite/Magento/Framework/Interception/AbstractPlugin.php @@ -5,6 +5,8 @@ */ namespace Magento\Framework\Interception; +use Magento\Framework\Code\Generator\Autoloader; +use Magento\Framework\ObjectManagerInterface; use Magento\Framework\App\Filesystem\DirectoryList; /** @@ -54,6 +56,7 @@ protected function setUp(): void protected function tearDown(): void { \Magento\Framework\App\ObjectManager::setInstance($this->applicationObjectManager); + $this->injectObjectManager($this->applicationObjectManager); } /** @@ -128,10 +131,35 @@ public function setUpInterceptionConfig($pluginConfig) 'preferences' => [ \Magento\Framework\Interception\PluginListInterface::class => \Magento\Framework\Interception\PluginList\PluginList::class, + \Magento\Framework\Interception\ChainInterface::class => + \Magento\Framework\Interception\Chain\Chain::class, + \Magento\Framework\Interception\Code\Generator\InterceptorInterface::class => + \Magento\Framework\Interception\Code\Generator\Interceptor::class, \Magento\Framework\Interception\ConfigWriterInterface::class => \Magento\Framework\Interception\PluginListGenerator::class ], ] ); + + $this->injectObjectManager($this->_objectManager); + } + + /** + * Inject object manager into autoloader. + * + * @param ObjectManagerInterface $objectManager + * @throws \ReflectionException + */ + private function injectObjectManager(ObjectManagerInterface $objectManager): void + { + foreach (spl_autoload_functions() as $autoloader) { + if (is_array($autoloader) && $autoloader[0] instanceof Autoloader) { + $autoloaderReflection = new \ReflectionClass($autoloader[0]); + $generatorProperty = $autoloaderReflection->getProperty('_generator'); + $generatorProperty->setAccessible(true); + $generatorProperty->getValue($autoloader[0])->setObjectManager($objectManager); + break; + } + } } } diff --git a/lib/internal/Magento/Framework/App/Filesystem/DirectoryList.php b/lib/internal/Magento/Framework/App/Filesystem/DirectoryList.php index ce150b00a6665..6768c29e8ac29 100644 --- a/lib/internal/Magento/Framework/App/Filesystem/DirectoryList.php +++ b/lib/internal/Magento/Framework/App/Filesystem/DirectoryList.php @@ -12,6 +12,11 @@ */ class DirectoryList extends \Magento\Framework\Filesystem\DirectoryList { + /** + * Path for compiled interceptors static cache directory. + */ + public const STATIC_CACHE = 'static_cache'; + /** * Code base root */ @@ -176,6 +181,7 @@ public static function getDefaultConfig() self::GENERATED => [parent::PATH => 'generated'], self::GENERATED_CODE => [parent::PATH => Io::DEFAULT_DIRECTORY], self::GENERATED_METADATA => [parent::PATH => 'generated/metadata'], + self::STATIC_CACHE => [parent::PATH => 'generated/static_cache'], self::VAR_IMPORT_EXPORT => [parent::PATH => 'var', parent::URL_PATH => 'import_export'], ]; return parent::getDefaultConfig() + $result; diff --git a/lib/internal/Magento/Framework/App/State/CleanupFiles.php b/lib/internal/Magento/Framework/App/State/CleanupFiles.php index c95caf8310b77..bd4988f55dc51 100644 --- a/lib/internal/Magento/Framework/App/State/CleanupFiles.php +++ b/lib/internal/Magento/Framework/App/State/CleanupFiles.php @@ -54,7 +54,8 @@ public function clearCodeGeneratedClasses() { return array_merge( $this->emptyDir(DirectoryList::GENERATED_CODE), - $this->emptyDir(DirectoryList::GENERATED_METADATA) + $this->emptyDir(DirectoryList::GENERATED_METADATA), + $this->emptyDir(DirectoryList::STATIC_CACHE) ); } diff --git a/lib/internal/Magento/Framework/App/Test/Unit/State/CleanupFilesTest.php b/lib/internal/Magento/Framework/App/Test/Unit/State/CleanupFilesTest.php index d55ca62407c33..dc53e15417716 100644 --- a/lib/internal/Magento/Framework/App/Test/Unit/State/CleanupFilesTest.php +++ b/lib/internal/Magento/Framework/App/Test/Unit/State/CleanupFilesTest.php @@ -38,12 +38,14 @@ public function testClearCodeGeneratedClasses() { $dir1 = $this->getDirectoryCleanMock(); $dir2 = $this->getDirectoryCleanMock(); - $this->filesystem->expects($this->exactly(2)) + $dir3 = $this->getDirectoryCleanMock(); + $this->filesystem->expects($this->exactly(3)) ->method('getDirectoryWrite') ->willReturnMap( [ [DirectoryList::GENERATED_CODE, DriverPool::FILE, $dir1], [DirectoryList::GENERATED_METADATA, DriverPool::FILE, $dir2], + [DirectoryList::STATIC_CACHE, DriverPool::FILE, $dir3], ] ); $this->object->clearCodeGeneratedClasses(); diff --git a/lib/internal/Magento/Framework/Code/Generator/Autoloader.php b/lib/internal/Magento/Framework/Code/Generator/Autoloader.php index 35c138147e9d3..8a57710afc700 100644 --- a/lib/internal/Magento/Framework/Code/Generator/Autoloader.php +++ b/lib/internal/Magento/Framework/Code/Generator/Autoloader.php @@ -89,6 +89,7 @@ private function tryToLogException(\Exception $exception): void try { $logger = ObjectManager::getInstance()->get(LoggerInterface::class); $logger->debug($exception->getMessage(), ['exception' => $exception]); + // phpcs:ignore Magento2.CodeAnalysis.EmptyBlock } catch (\Exception $ignoreThisException) { // Do not take an action here, since the original exception might have been caused by logger } diff --git a/lib/internal/Magento/Framework/CompiledInterception/Generator/AreasPluginList.php b/lib/internal/Magento/Framework/CompiledInterception/Generator/AreasPluginList.php new file mode 100644 index 0000000000000..1995ed4965224 --- /dev/null +++ b/lib/internal/Magento/Framework/CompiledInterception/Generator/AreasPluginList.php @@ -0,0 +1,87 @@ +scope = $scope; + $this->staticScopeFactory = $staticScopeFactory; + $this->compiledPluginListFactory = $compiledPluginListFactory; + $this->plugins = $plugins; + } + + /** + * Get array of plugins config indexed by scope code + * + * @return array + */ + public function getPluginsConfigForAllAreas() + { + if ($this->plugins === null) { + $this->plugins = []; + //this is to emulate order M2 is reading scopes config to use scope cache + //"global|primary" should be loaded first and then "global|primary|frontend" etc. + $defaultScopePluginList = $defaultScope = null; + foreach ($this->scope->getAllScopes() as $scope) { + $configScope = $this->staticScopeFactory->create( + [ + 'scope' => $scope, + ] + ); + if ($defaultScopePluginList === null) { + $defaultScopePluginList = $this->compiledPluginListFactory->create(); + $defaultScopePluginList->setScope($configScope); + $defaultScopePluginList->getNext('dummy', 'dummy'); + $defaultScope = $scope; + } else { + $this->plugins[$scope] = clone $defaultScopePluginList; + $this->plugins[$scope]->setScope($configScope); + } + } + $this->plugins[$defaultScope] = $defaultScopePluginList; + } + + return $this->plugins; + } +} diff --git a/lib/internal/Magento/Framework/CompiledInterception/Generator/CompiledInterceptor.php b/lib/internal/Magento/Framework/CompiledInterception/Generator/CompiledInterceptor.php new file mode 100644 index 0000000000000..d69275fc39c1d --- /dev/null +++ b/lib/internal/Magento/Framework/CompiledInterception/Generator/CompiledInterceptor.php @@ -0,0 +1,702 @@ +areasPlugins = $areasPlugins; + } + + /** + * Unused function required by production mode interface + * + * @param mixed $interceptedMethods + */ + public function setInterceptedMethods($interceptedMethods) + { + // this is not used as methods are read from reflection + $this->interceptedMethods = $interceptedMethods; + } + + /** + * Get properties to be set in constructor. + * + * @return array + */ + public static function propertiesToSetInConstructor() + { + return [ + ScopeInterface::class => '____scope', + ObjectManagerInterface::class => '____om', + ]; + } + + /** + * Get all class methods + * + * @return array|null + * @throws \ReflectionException + */ + protected function _getClassMethods() + { + $this->generateMethodsAndProperties(); + return $this->classMethods; + } + + /** + * Get all class properties + * + * @return array|null + * @throws \ReflectionException + */ + protected function _getClassProperties() + { + $this->generateMethodsAndProperties(); + return $this->classProperties; + } + + /** + * Get default constructor definition for generated class + * + * @return array + * @throws \ReflectionException + */ + protected function _getDefaultConstructorDefinition() + { + return $this->injectPropertiesSettersToConstructor( + $this->getSourceClassReflection()->getConstructor(), + static::propertiesToSetInConstructor() + ); + } + + /** + * Generate class source + * + * @return bool|string + * @throws \ReflectionException + */ + protected function _generateCode() + { + if ($this->getSourceClassReflection()->isInterface()) { + return false; + } else { + $this->_classGenerator->setExtendedClass($this->getSourceClassName()); + } + $this->generateMethodsAndProperties(); + return parent::_generateCode(); + } + + /** + * Generate all methods and properties + * + * @throws \ReflectionException + */ + private function generateMethodsAndProperties() + { + if ($this->classMethods === null) { + $this->classMethods = []; + $this->classProperties = []; + + foreach (static::propertiesToSetInConstructor() as $type => $name) { + $this->_classGenerator->addUse($type); + $this->classProperties[] = [ + 'name' => $name, + 'visibility' => 'private', + 'docblock' => [ + 'tags' => [['name' => 'var', 'description' => substr(strrchr($type, "\\"), 1)]], + ] + ]; + } + $this->classMethods[] = $this->_getDefaultConstructorDefinition(); + $this->overrideMethodsAndGeneratePluginGetters($this->getSourceClassReflection()); + } + } + + /** + * Get reflection of source class + * + * @return \ReflectionClass + * @throws \ReflectionException + */ + private function getSourceClassReflection() + { + if ($this->baseReflection === null) { + $this->baseReflection = new \ReflectionClass($this->getSourceClassName()); + } + return $this->baseReflection; + } + + /** + * Whether method is intercepted + * + * @param \ReflectionMethod $method + * @return bool + */ + private function isInterceptedMethod(\ReflectionMethod $method) + { + return !($method->isConstructor() || $method->isFinal() || $method->isStatic() || $method->isDestructor()) && + !in_array($method->getName(), ['__sleep', '__wakeup', '__clone']); + } + + /** + * Generate compiled methods and plugin getters + * + * @param \ReflectionClass $reflection + */ + private function overrideMethodsAndGeneratePluginGetters(\ReflectionClass $reflection) + { + $publicMethods = $reflection->getMethods(\ReflectionMethod::IS_PUBLIC); + + $allPlugins = []; + foreach ($publicMethods as $method) { + if ($this->isInterceptedMethod($method)) { + $config = $this->getPluginsConfig($method, $allPlugins); + if (!empty($config)) { + $this->classMethods[] = $this->getCompiledMethodInfo($method, $config); + } + } + } + foreach ($allPlugins as $plugins) { + foreach ($plugins as $plugin) { + $this->classMethods[] = $this->getPluginGetterInfo($plugin); + } + } + } + + /** + * Generate class constructor adding required properties when types are not present in parent constructor + * + * @param \ReflectionMethod|null $parentConstructor + * @param array $properties + * @return array + */ + private function injectPropertiesSettersToConstructor(\ReflectionMethod $parentConstructor = null, $properties = []) + { + if ($parentConstructor == null) { + $parameters = []; + $body = []; + } else { + $parameters = $parentConstructor->getParameters(); + $parentCallParams = []; + foreach ($parameters as $parameter) { + $parentCallParams[] = '$' . $parameter->getName(); + } + $body = ["parent::__construct(" . implode(', ', $parentCallParams) . ");"]; + } + $extraSetters = array_fill_keys(array_values($properties), null); + foreach ($parameters as $parameter) { + if ($parameter->getType()) { + $type = $parameter->getType()->getName(); + if (isset($properties[$type])) { + $extraSetters[$properties[$type]] = $parameter->getName(); + } + } + } + $parameters = array_map([$this, '_getMethodParameterInfo'], $parameters); + foreach ($extraSetters as $name => $paramName) { + $class = array_search($name, $properties); + if ($paramName !== null) { + array_unshift( + $body, + "\$this->$name = \$$paramName;" + ); + } elseif ($class === ObjectManagerInterface::class) { + array_unshift( + $body, + "\$this->$name = \Magento\Framework\App\ObjectManager::getInstance();" + ); + } else { + array_unshift( + $body, + "\$this->$name = \Magento\Framework\App\ObjectManager::getInstance()->get(\\$class::class);" + ); + } + } + return [ + 'name' => '__construct', + 'parameters' => $parameters, + 'body' => implode("\n", $body), + 'docblock' => ['shortDescription' => '@inheritdoc'], + ]; + } + + /** + * Adds tabulation to nested code block + * + * @param array $body + * @param array $sub + * @param int $indent + */ + private function addCodeSubBlock(&$body, $sub, $indent = 1) + { + foreach ($sub as $line) { + $body[] = ('' === $line) ? '' : str_repeat("\t", $indent) . $line; + } + } + + /** + * Generate source of before plugins + * + * @param array $plugins + * @param string $methodName + * @param string $extraParams + * @param string $parametersList + * @return array + */ + private function compileBeforePlugins($plugins, $methodName, $extraParams, $parametersList) + { + $lines = []; + foreach ($plugins as $plugin) { + $call = "\$this->" . $this->getGetterName($plugin) . "()->$methodName(\$this$extraParams);"; + + if (!empty($parametersList)) { + $lines[] = "\$beforeResult = " . $call; + $lines[] = "if (\$beforeResult !== null) list({$parametersList}) = (array)\$beforeResult;"; + } else { + $lines[] = $call; + } + $lines[] = ""; + } + return $lines; + } + + /** + * Generate source of around plugin + * + * @param string $methodName + * @param array $plugin + * @param string $capitalizedName + * @param string $extraParams + * @param array $parameters + * @param bool $returnVoid + * @return array + */ + private function compileAroundPlugin($methodName, $plugin, $capitalizedName, $extraParams, $parameters, $returnVoid) + { + $lines = []; + $lines[] = "\$this->{$this->getGetterName($plugin)}()->around$capitalizedName" . + "(\$this, function({$this->getParameterListForNextCallback($parameters)}){"; + $this->addCodeSubBlock( + $lines, + $this->getMethodSourceFromConfig($methodName, $plugin['next'] ?: [], $parameters, $returnVoid) + ); + $lines[] = "}$extraParams);"; + return $lines; + } + + /** + * Generate source of after plugins + * + * @param array $plugins + * @param string $methodName + * @param string $extraParams + * @param bool $returnVoid + * @return array + */ + private function compileAfterPlugins($plugins, $methodName, $extraParams, $returnVoid) + { + $lines = []; + foreach ($plugins as $plugin) { + $call = "\$this->" . $this->getGetterName($plugin) . "()->$methodName(\$this, "; + + if (!$returnVoid) { + $lines[] = ["$call\$result$extraParams);"]; + } else { + $lines[] = ["{$call}null$extraParams);"]; + } + } + return $lines; + } + + /** + * Generate interceptor source using config + * + * @param string $methodName + * @param array $conf + * @param array $parameters + * @param bool $returnVoid + * @return array + */ + private function getMethodSourceFromConfig($methodName, $conf, $parameters, $returnVoid) + { + $capitalizedName = ucfirst($methodName); + $parametersList = $this->getParameterList($parameters); + $extraParams = empty($parameters) ? '' : (', ' . $parametersList); + + if (isset($conf[DefinitionInterface::LISTENER_BEFORE])) { + $body = $this->compileBeforePlugins( + $conf[DefinitionInterface::LISTENER_BEFORE], + 'before' . $capitalizedName, + $extraParams, + $parametersList + ); + } else { + $body = []; + } + + $resultChain = []; + if (isset($conf[DefinitionInterface::LISTENER_AROUND])) { + $resultChain[] = $this->compileAroundPlugin( + $methodName, + $conf[DefinitionInterface::LISTENER_AROUND], + $capitalizedName, + $extraParams, + $parameters, + $returnVoid + ); + } else { + $resultChain[] = ["parent::{$methodName}({$this->getParameterList($parameters)});"]; + } + + if (isset($conf[DefinitionInterface::LISTENER_AFTER])) { + $resultChain = array_merge( + $resultChain, + $this->compileAfterPlugins( + $conf[DefinitionInterface::LISTENER_AFTER], + 'after' . $capitalizedName, + $extraParams, + $returnVoid + ) + ); + } + return array_merge($body, $this->getResultChainLines($resultChain, $returnVoid)); + } + + /** + * Implode result chain into list of assignments + * + * @param array $resultChain + * @param bool $returnVoid + * @return array + */ + private function getResultChainLines($resultChain, $returnVoid) + { + $lines = []; + $first = true; + foreach ($resultChain as $lp => $piece) { + if ($first) { + $first = false; + } else { + $lines[] = ""; + } + if (!$returnVoid) { + $piece[0] = (($lp + 1 == count($resultChain)) ? "return " : "\$result = ") . $piece[0]; + } + foreach ($piece as $line) { + $lines[] = $line; + } + } + return $lines; + } + + /** + * Get parameters definition for next callback + * + * @param array $parameters + * @return string + */ + private function getParameterListForNextCallback(array $parameters) + { + $ret = []; + foreach ($parameters as $parameter) { + $ret [] = + ($parameter->isPassedByReference() ? '&' : '') . + "\${$parameter->getName()}" . + ($parameter->isDefaultValueAvailable() ? + ' = ' . ($parameter->isDefaultValueConstant() ? + $parameter->getDefaultValueConstantName() : + str_replace("\n", '', var_export($parameter->getDefaultValue(), true))) : + ''); + } + return implode(', ', $ret); + } + + /** + * Implodes parameters into list for call + * + * @param array(\ReflectionParameter) $parameters + * @return string + */ + private function getParameterList(array $parameters) + { + $ret = []; + foreach ($parameters as $parameter) { + $ret [] = "\${$parameter->getName()}"; + } + return implode(', ', $ret); + } + + /** + * Get plugin getter name + * + * @param array $plugin + * @return string + */ + private function getGetterName($plugin) + { + return '____plugin_' . $plugin['clean_name']; + } + + /** + * Prepares plugin getter for code generator + * + * @param array $plugin + * @return array + */ + private function getPluginGetterInfo($plugin) + { + return [ + 'name' => $this->getGetterName($plugin), + 'visibility' => 'private', + 'parameters' => [], + 'body' => "return \$this->____om->get(\\" . "{$plugin['class']}::class);", + 'docblock' => [ + 'shortDescription' => 'plugin "' . $plugin['code'] . '"' . "\n" . '@return \\' . $plugin['class'] + ], + ]; + } + + /** + * Get compiled method data for code generator + * + * @param \ReflectionMethod $method + * @param array $config + * @return array + */ + private function getCompiledMethodInfo(\ReflectionMethod $method, $config) + { + $parameters = $method->getParameters(); + $returnsVoid = ($method->hasReturnType() && $method->getReturnType()->getName() == 'void'); + + $cases = $this->getScopeCasesFromConfig($config); + + if (count($cases) == 1) { + $body = $this->getMethodSourceFromConfig($method->getName(), $cases[0]['conf'], $parameters, $returnsVoid); + } else { + $body = [ + 'switch ($this->____scope->getCurrentScope()) {' + ]; + + foreach ($cases as $case) { + // phpcs:ignore Magento2.Performance.ForeachArrayMerge + $body = array_merge($body, $case['cases']); + $this->addCodeSubBlock( + $body, + $this->getMethodSourceFromConfig($method->getName(), $case['conf'], $parameters, $returnsVoid), + 2 + ); + if ($returnsVoid) { + $body[] = "\t\tbreak;"; + } + } + + $body[] = "}"; + } + + $returnType = $method->getReturnType(); + $returnTypeValue = $returnType + ? ($returnType->allowsNull() ? '?' : '') . $returnType->getName() + : null; + if ($returnTypeValue === 'self') { + $returnTypeValue = $this->_getFullyQualifiedClassName($method->getDeclaringClass()->getName()); + } + return [ + 'name' => ($method->returnsReference() ? '& ' : '') . $method->getName(), + 'parameters' =>array_map([$this, '_getMethodParameterInfo'], $parameters), + 'body' => implode("\n", $body), + 'returnType' => $returnTypeValue, + 'docblock' => ['shortDescription' => '@inheritdoc'], + ]; + } + + /** + * Get scope cases from config + * + * @param array $config + * @return array + */ + private function getScopeCasesFromConfig($config) + { + $cases = []; + //group cases by config + foreach ($config as $scope => $conf) { + $caseStr = "\tcase '$scope':"; + foreach ($cases as &$case) { + if ($case['conf'] == $conf) { + $case['cases'][] = $caseStr; + continue 2; + } + } + $cases[] = ['cases'=>[$caseStr], 'conf'=>$conf]; + } + $cases[count($cases) - 1]['cases'] = ["\tdefault:"]; + return $cases; + } + + /** + * Generate array with plugin info + * + * @param CompiledPluginList $plugins + * @param string $code + * @param string $className + * @param array $allPlugins + * @param string|null $next + * @return mixed + */ + private function getPluginInfo(CompiledPluginList $plugins, $code, $className, &$allPlugins, $next = null) + { + $className = $plugins->getPluginType($className, $code); + if (!isset($allPlugins[$code])) { + $allPlugins[$code] = []; + } + if (empty($allPlugins[$code][$className])) { + $suffix = count($allPlugins[$code]) ? count($allPlugins[$code]) + 1 : ''; + $allPlugins[$code][$className] = [ + 'code' => $code, + 'class' => $className, + 'clean_name' => preg_replace("/[^A-Za-z0-9_]/", '_', $code . $suffix) + ]; + } + $result = $allPlugins[$code][$className]; + $result['next'] = $next; + return $result; + } + + /** + * Get next set of plugins + * + * @param CompiledPluginList $plugins + * @param string $className + * @param string $method + * @param array $allPlugins + * @param string $next + * @return array + */ + private function getPluginsChain(CompiledPluginList $plugins, $className, $method, &$allPlugins, $next = '__self') + { + $result = $plugins->getNext($className, $method, $next); + if (!empty($result[DefinitionInterface::LISTENER_BEFORE])) { + foreach ($result[DefinitionInterface::LISTENER_BEFORE] as $k => $code) { + $result[DefinitionInterface::LISTENER_BEFORE][$k] = $this->getPluginInfo( + $plugins, + $code, + $className, + $allPlugins + ); + } + } + if (!empty($result[DefinitionInterface::LISTENER_AFTER])) { + foreach ($result[DefinitionInterface::LISTENER_AFTER] as $k => $code) { + $result[DefinitionInterface::LISTENER_AFTER][$k] = $this->getPluginInfo( + $plugins, + $code, + $className, + $allPlugins + ); + } + } + if (isset($result[DefinitionInterface::LISTENER_AROUND])) { + $result[DefinitionInterface::LISTENER_AROUND] = $this->getPluginInfo( + $plugins, + $result[DefinitionInterface::LISTENER_AROUND], + $className, + $allPlugins, + $this->getPluginsChain( + $plugins, + $className, + $method, + $allPlugins, + $result[DefinitionInterface::LISTENER_AROUND] + ) + ); + } + return $result; + } + + /** + * Generates recursive maps of plugins for given method + * + * @param \ReflectionMethod $method + * @param array $allPlugins + * @return array + */ + private function getPluginsConfig(\ReflectionMethod $method, &$allPlugins) + { + $className = ltrim($this->getSourceClassName(), '\\'); + + $result = []; + foreach ($this->areasPlugins->getPluginsConfigForAllAreas() as $scope => $pluginsList) { + $pluginChain = $this->getPluginsChain($pluginsList, $className, $method->getName(), $allPlugins); + if ($pluginChain) { + $result[$scope] = $pluginChain; + } + } + //if plugins are not empty make sure default case will be handled + if (!empty($result) && !$pluginChain) { + $result[$scope] = []; + } + return $result; + } +} diff --git a/lib/internal/Magento/Framework/CompiledInterception/Generator/CompiledPluginList.php b/lib/internal/Magento/Framework/CompiledInterception/Generator/CompiledPluginList.php new file mode 100644 index 0000000000000..062449125f7d6 --- /dev/null +++ b/lib/internal/Magento/Framework/CompiledInterception/Generator/CompiledPluginList.php @@ -0,0 +1,98 @@ +pluginList = $pluginList; + } + + /** + * Retrieve plugin Instance + * + * @param string $type + * @param string $code + * @return mixed + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function getPlugin($type, $code) + { + return null; + } + + /** + * Merge configuration + * + * @param array $config + * @return void + */ + public function merge(array $config) + { + $this->pluginList->merge($config); + } + + /** + * Get class of a plugin + * + * @param string $type + * @param string $code + * @return mixed + */ + public function getPluginType(string $type, string $code) + { + return $this->pluginList->getPluginType($type, $code); + } + + /** + * Set current scope + * + * @param ScopeInterface $scope + */ + public function setScope(ScopeInterface $scope) + { + $this->pluginList->setScope($scope); + } + + /** + * Retrieve next plugins in chain + * + * @param string $type + * @param string $method + * @param string $code + * @return array + */ + public function getNext($type, $method, $code = null) + { + return $this->pluginList->getNext($type, $method, $code); + } + + /** + * PluginList instance should not be shared. + */ + public function __clone() + { + $this->pluginList = clone $this->pluginList; + } +} diff --git a/lib/internal/Magento/Framework/CompiledInterception/Generator/FileCache.php b/lib/internal/Magento/Framework/CompiledInterception/Generator/FileCache.php new file mode 100644 index 0000000000000..3b94c1b7f5982 --- /dev/null +++ b/lib/internal/Magento/Framework/CompiledInterception/Generator/FileCache.php @@ -0,0 +1,167 @@ +directoryList = $directoryList; + $this->cachePath = $cachePath; + } + + /** + * Test if a cache is available for the given id + * + * @param string $identifier Cache id + * @return int|bool Last modified time of cache entry if it is available, false otherwise + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function test($identifier) + { + return file_exists($this->getCacheFilePath($identifier)); + } + + /** + * Load cache record by its unique identifier + * + * @param string $identifier + * @return string|bool + * @SuppressWarnings(PHPMD) + */ + public function load($identifier) + { + // @codingStandardsIgnoreLine + return $this->getCachePath() ? @include $this->getCacheFilePath($identifier) : false; + } + + /** + * Save cache record + * + * @param string $data + * @param string $identifier + * @param array $tags + * @param int|bool|null $lifeTime + * @return bool + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function save($data, $identifier, array $tags = [], $lifeTime = null) + { + if ($this->getCachePath()) { + $path = $this->getCacheFilePath($identifier); + if (!is_dir(dirname($path))) { + mkdir(dirname($path), 0777, true); + } + file_put_contents( + $path, + '' + ); + return true; + } + } + + /** + * Remove cache record by its unique identifier + * + * @param string $identifier + * @return bool + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function remove($identifier) + { + return false; + } + + /** + * Clean cache records matching specified tags + * + * @param string $mode + * @param array $tags + * @return bool + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function clean($mode = \Zend_Cache::CLEANING_MODE_ALL, array $tags = []) + { + if ($this->getCachePath()) { + foreach (glob($this->getCachePath() . '/*') as $file) { + if (is_file($file)) { + unlink($file); + } + } + return true; + } + return false; + } + + /** + * Retrieve backend instance + * + * @return \Zend_Cache_Backend_Interface + */ + public function getBackend() + { + return null; + } + + /** + * Retrieve frontend instance compatible with Zend Locale Data setCache() to be used as a workaround + * + * @return \Zend_Cache_Core + */ + public function getLowLevelFrontend() + { + return null; + } + + /** + * Get file cache path. + * + * @param string $identifier + * @return string + */ + private function getCacheFilePath($identifier): string + { + $identifier = str_replace('|', '_', $identifier); + return $this->getCachePath() . DIRECTORY_SEPARATOR . $identifier . '.php'; + } + + /** + * Get cache path. + * + * @return string + */ + private function getCachePath(): string + { + if (!$this->cachePath) { + $this->cachePath = $this->directoryList->getPath(DirectoryList::STATIC_CACHE); + } + + return $this->cachePath; + } +} diff --git a/lib/internal/Magento/Framework/CompiledInterception/Generator/NoSerialize.php b/lib/internal/Magento/Framework/CompiledInterception/Generator/NoSerialize.php new file mode 100644 index 0000000000000..dca9428d1e6b5 --- /dev/null +++ b/lib/internal/Magento/Framework/CompiledInterception/Generator/NoSerialize.php @@ -0,0 +1,40 @@ +scope = $scope; + } + + /** + * Get current configuration scope identifier + * + * @return string + */ + public function getCurrentScope() + { + return $this->scope; + } + + /** + * Unused interface method + * + * @param string $scope + */ + public function setCurrentScope($scope) + { + $this->scope = $scope; + } +} diff --git a/lib/internal/Magento/Framework/CompiledInterception/README.md b/lib/internal/Magento/Framework/CompiledInterception/README.md new file mode 100644 index 0000000000000..77b0bb52f47f1 --- /dev/null +++ b/lib/internal/Magento/Framework/CompiledInterception/README.md @@ -0,0 +1,88 @@ +### ABOUT + +This component changes the way Magento 2 generates Interceptor classes (a mechanism that allows plugins to work together). + +Instead of generating boilerplate code it compiles the Interceptor using information from source code. +This makes plugins slightly faster and as Magento uses a lot of plugins, even at its core, it lowers request time by ~10%. +This is important in places where there is a lot of non-cached PHP logic going on (for example admin panel is noticeably faster). + +The default method uses code that is called on nearly each method to see if there are any plugins connected, in generated code this is not required and call-stack is reduced. + +Having plugins called directly also makes code easier to debug and bugs easier to find. + +The Interceptors generated by this plugin are 100% compatible with the ones generated by Magento by default, so there is no need to change anything in your plugins. + +### ENABLING + +To use compiled interceptors please add following preference to your di.xml +``` + + + + + Magento\Framework\CompiledInterception\Generator\CompiledInterceptor + + +``` + +Clear generated files and cache: + +`rm -rf generated/* && bin/magento cache:clean` + +### DISABLING + +Remove preferences from `app/etc/di.xml`, remove module and clear cache and generated files. + +### TECHNICAL DETAILS + +Instead of interceptors that read plugins config at runtime like this: + +``` +public function methodX($arg) { + $pluginInfo = $this->pluginList->getNext($this->subjectType, 'methodX'); + if (!$pluginInfo) { + return parent::methodX($arg); + } else { + return $this->___callPlugins('methodX', func_get_args(), $pluginInfo); + } +} +``` + +This generator generates static interceptors like this: + + +``` +public function methodX($arg) { + switch(getCurrentScope()){ + case 'frontend': + $this->_get_example_plugin()->beforeMethodX($this, $arg); + $this->_get_another_plugin()->beforeMethodX($this, $arg); + $result = $this->_get_around_plugin()->aroundMethodX($this, function($arg){ + return parent::methodX($arg); + }); + return $this->_get_after_plugin()->afterMethodX($this, $result); + case 'adminhtml': + // ... + default: + return parent::methodX($arg); + } +} +``` + + +#### PROS + +* Easier debugging. + * If you ever stumbled upon `___callPlugins` when debugging you should know how painful it is to debug issues inside plugins. + * Generated code is decorated with PHPDoc for easier debugging in IDE + +* Fastest response time (5%-15% faster in developer and production mode) + * No redundant calls to `___callPlugins` in call stack. + * Methods with no plugins are not overridden in parent at all. + +* Implemented as a module and can be easily reverted to the default `Generator\Interceptor` + +#### CONS + +* Each time after making change in etc plugins config, `generated/code/*` and `var/cache/*` needs to be purged +* As this does not load plugins at runtime, might not work in an edge case of plugging into core Magento classes like `PluginsList` etc. diff --git a/lib/internal/Magento/Framework/DB/Adapter/Pdo/CallbackPool.php b/lib/internal/Magento/Framework/DB/Adapter/Pdo/CallbackPool.php new file mode 100644 index 0000000000000..d35e4ddf948d7 --- /dev/null +++ b/lib/internal/Magento/Framework/DB/Adapter/Pdo/CallbackPool.php @@ -0,0 +1,57 @@ +string = $string; $this->dateTime = $dateTime; @@ -278,6 +286,8 @@ public function __construct( } catch (Zend_Db_Adapter_Exception $e) { throw new \InvalidArgumentException($e->getMessage(), $e->getCode(), $e); } + $this->executeCommitCallbacks = $executeCommitCallbacks + ?? ObjectManager::getInstance()->get(ExecuteCommitCallbacks::class); } /** @@ -321,7 +331,8 @@ public function commit() throw new \Exception(AdapterInterface::ERROR_ROLLBACK_INCOMPLETE_MESSAGE); } --$this->_transactionLevel; - return $this; + + return $this->executeCommitCallbacks->afterCommit($this); } /** @@ -344,7 +355,8 @@ public function rollBack() $this->_isRolledBack = true; } --$this->_transactionLevel; - return $this; + + return $this->executeCommitCallbacks->afterRollBack($this); } /** diff --git a/lib/internal/Magento/Framework/EntityManager/CallbackHandler.php b/lib/internal/Magento/Framework/EntityManager/CallbackHandler.php index 255257fd4ac3d..8d401ca8cb536 100644 --- a/lib/internal/Magento/Framework/EntityManager/CallbackHandler.php +++ b/lib/internal/Magento/Framework/EntityManager/CallbackHandler.php @@ -6,12 +6,9 @@ namespace Magento\Framework\EntityManager; -use Magento\Framework\Model\CallbackPool; +use Magento\Framework\DB\Adapter\Pdo\CallbackPool; use Psr\Log\LoggerInterface; -/** - * Class CallbackHandler - */ class CallbackHandler { /** @@ -39,6 +36,8 @@ public function __construct( } /** + * Process entity type. + * * @param string $entityType * @throws \Exception * @return void @@ -62,6 +61,8 @@ public function process($entityType) } /** + * Attach entity type to callback pool. + * * @param string $entityType * @param array $callback * @throws \Exception @@ -74,6 +75,8 @@ public function attach($entityType, $callback) } /** + * Remove entity type from callback pool. + * * @param string $entityType * @throws \Exception * @return void diff --git a/lib/internal/Magento/Framework/Interception/Code/Generator/Interceptor.php b/lib/internal/Magento/Framework/Interception/Code/Generator/Interceptor.php index 4a3ed34810723..14f9563f43fbc 100644 --- a/lib/internal/Magento/Framework/Interception/Code/Generator/Interceptor.php +++ b/lib/internal/Magento/Framework/Interception/Code/Generator/Interceptor.php @@ -9,7 +9,7 @@ use Magento\Framework\Code\Generator\EntityAbstract; -class Interceptor extends EntityAbstract +class Interceptor extends EntityAbstract implements InterceptorInterface { public const ENTITY_TYPE = 'interceptor'; diff --git a/lib/internal/Magento/Framework/Interception/Code/Generator/InterceptorInterface.php b/lib/internal/Magento/Framework/Interception/Code/Generator/InterceptorInterface.php new file mode 100644 index 0000000000000..4cf23952d8864 --- /dev/null +++ b/lib/internal/Magento/Framework/Interception/Code/Generator/InterceptorInterface.php @@ -0,0 +1,21 @@ +_data = $this->pluginListGenerator->merge($config, $this->_data); } + + /** + * Get class of a plugin + * + * @param string $type + * @param string $code + * @return mixed + */ + public function getPluginType(string $type, string $code) + { + return $this->_inherited[$type][$code]['instance']; + } + + /** + * Set current scope + * + * @param ScopeInterface $scope + */ + public function setScope(ScopeInterface $scope) + { + $this->_configScope = $scope; + } } diff --git a/lib/internal/Magento/Framework/Model/CallbackPool.php b/lib/internal/Magento/Framework/Model/CallbackPool.php index c4693e5575020..e908f368f8e06 100644 --- a/lib/internal/Magento/Framework/Model/CallbackPool.php +++ b/lib/internal/Magento/Framework/Model/CallbackPool.php @@ -3,51 +3,48 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); namespace Magento\Framework\Model; +use Magento\Framework\DB\Adapter\Pdo\CallbackPool as PdoCallbackPool; + /** - * Class CallbackPool + * @deprecated please use Magento\Framework\DB\Adapter\Pdo\CallbackPool. */ class CallbackPool { /** - * Array of callbacks subscribed to commit transaction commit + * Add callback by hash key. * - * @var array - */ - private static $commitCallbacks = []; - - /** * @param string $hashKey * @param array $callback * @return void */ public static function attach($hashKey, $callback) { - self::$commitCallbacks[$hashKey][] = $callback; + PdoCallbackPool::attach($hashKey, $callback); } /** + * Remove callbacks by hash key. + * * @param string $hashKey * @return void */ public static function clear($hashKey) { - self::$commitCallbacks[$hashKey] = []; + PdoCallbackPool::clear($hashKey); } /** + * Get callbacks by hash key. + * * @param string $hashKey * @return array */ public static function get($hashKey) { - if (!isset(self::$commitCallbacks[$hashKey])) { - return []; - } - $callbacks = self::$commitCallbacks[$hashKey]; - self::$commitCallbacks[$hashKey] = []; - return $callbacks; + return PdoCallbackPool::get($hashKey); } } diff --git a/lib/internal/Magento/Framework/Model/ExecuteCommitCallbacks.php b/lib/internal/Magento/Framework/Model/ExecuteCommitCallbacks.php index 799f8ffda253c..96837d57225f5 100644 --- a/lib/internal/Magento/Framework/Model/ExecuteCommitCallbacks.php +++ b/lib/internal/Magento/Framework/Model/ExecuteCommitCallbacks.php @@ -8,6 +8,7 @@ namespace Magento\Framework\Model; use Magento\Framework\DB\Adapter\AdapterInterface; +use Magento\Framework\DB\Adapter\Pdo\CallbackPool; use Psr\Log\LoggerInterface; /** @@ -31,14 +32,13 @@ public function __construct(LoggerInterface $logger) /** * Execute callbacks after commit. * - * @param AdapterInterface $subject - * @param AdapterInterface $result + * @param AdapterInterface $adapter * @return AdapterInterface */ - public function afterCommit(AdapterInterface $subject, AdapterInterface $result): AdapterInterface + public function afterCommit(AdapterInterface $adapter): AdapterInterface { - if ($result->getTransactionLevel() === 0) { - $callbacks = CallbackPool::get(spl_object_hash($subject)); + if ($adapter->getTransactionLevel() === 0) { + $callbacks = CallbackPool::get(spl_object_hash($adapter)); foreach ($callbacks as $callback) { try { call_user_func($callback); @@ -48,20 +48,19 @@ public function afterCommit(AdapterInterface $subject, AdapterInterface $result) } } - return $result; + return $adapter; } /** * Drop callbacks after rollBack. * - * @param AdapterInterface $subject - * @param AdapterInterface $result + * @param AdapterInterface $adapter * @return AdapterInterface */ - public function afterRollBack(AdapterInterface $subject, AdapterInterface $result): AdapterInterface + public function afterRollBack(AdapterInterface $adapter): AdapterInterface { - CallbackPool::clear(spl_object_hash($subject)); + CallbackPool::clear(spl_object_hash($adapter)); - return $result; + return $adapter; } } diff --git a/lib/internal/Magento/Framework/Model/ResourceModel/AbstractResource.php b/lib/internal/Magento/Framework/Model/ResourceModel/AbstractResource.php index 221110217a200..691d97c9e957b 100644 --- a/lib/internal/Magento/Framework/Model/ResourceModel/AbstractResource.php +++ b/lib/internal/Magento/Framework/Model/ResourceModel/AbstractResource.php @@ -7,7 +7,7 @@ use Magento\Framework\App\ObjectManager; use Magento\Framework\DataObject; -use Magento\Framework\Model\CallbackPool; +use Magento\Framework\DB\Adapter\Pdo\CallbackPool; use Magento\Framework\Serialize\Serializer\Json; /** diff --git a/lib/internal/Magento/Framework/ObjectManager/DefinitionFactory.php b/lib/internal/Magento/Framework/ObjectManager/DefinitionFactory.php index 492af22815311..d0f498e26db6b 100644 --- a/lib/internal/Magento/Framework/ObjectManager/DefinitionFactory.php +++ b/lib/internal/Magento/Framework/ObjectManager/DefinitionFactory.php @@ -5,9 +5,9 @@ */ namespace Magento\Framework\ObjectManager; +use Magento\Framework\Code\Generator\Autoloader; use Magento\Framework\Filesystem\DriverInterface; use Magento\Framework\ObjectManager\Definition\Runtime; -use Magento\Framework\Code\Generator\Autoloader; /** * @SuppressWarnings(PHPMD.CouplingBetweenObjects) @@ -52,8 +52,16 @@ public function __construct( */ public function createClassDefinition() { + foreach (spl_autoload_functions() as $autoloader) { + if (is_array($autoloader) && $autoloader[0] instanceof \Magento\Framework\Code\Generator\Autoloader) { + spl_autoload_unregister($autoloader); + break; + } + } + $autoloader = new Autoloader($this->getCodeGenerator()); spl_autoload_register([$autoloader, 'load']); + return new Runtime(); } @@ -91,6 +99,7 @@ public function getCodeGenerator() ); $this->codeGenerator = new \Magento\Framework\Code\Generator($generatorIo); } + return $this->codeGenerator; } } diff --git a/lib/internal/Magento/Framework/ObjectManager/Factory/Compiled.php b/lib/internal/Magento/Framework/ObjectManager/Factory/Compiled.php index b219d93b0f0fa..64a67c0bb39fe 100644 --- a/lib/internal/Magento/Framework/ObjectManager/Factory/Compiled.php +++ b/lib/internal/Magento/Framework/ObjectManager/Factory/Compiled.php @@ -5,6 +5,9 @@ */ namespace Magento\Framework\ObjectManager\Factory; +/** + * Compiled factory. + */ class Compiled extends AbstractFactory { /** diff --git a/setup/src/Magento/Setup/Console/Command/DiCompileCommand.php b/setup/src/Magento/Setup/Console/Command/DiCompileCommand.php index 4c50a3de4fb31..e2231ed142285 100644 --- a/setup/src/Magento/Setup/Console/Command/DiCompileCommand.php +++ b/setup/src/Magento/Setup/Console/Command/DiCompileCommand.php @@ -10,19 +10,21 @@ use Magento\Framework\Filesystem\Io\File; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; -use Magento\Framework\Filesystem; -use Magento\Framework\App\Filesystem\DirectoryList; use Magento\Framework\App\DeploymentConfig; +use Magento\Framework\App\Filesystem\DirectoryList; +use Magento\Framework\App\ObjectManager\ConfigWriterInterface; use Magento\Framework\Component\ComponentRegistrar; use Magento\Framework\Config\ConfigOptionsListConstants; +use Magento\Framework\Console\Cli; +use Magento\Framework\Filesystem; use Magento\Setup\Model\ObjectManagerProvider; use Magento\Setup\Module\Di\App\Task\Manager; -use Magento\Setup\Module\Di\App\Task\OperationFactory; use Magento\Setup\Module\Di\App\Task\OperationException; +use Magento\Setup\Module\Di\App\Task\OperationFactory; use Magento\Setup\Module\Di\App\Task\OperationInterface; +use Magento\Setup\Module\Di\Compiler\Config\Chain\InterceptorSubstitutionInterface; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Helper\ProgressBar; -use Magento\Framework\Console\Cli; /** * Command to run compile in single-tenant mode @@ -135,7 +137,7 @@ private function checkEnvironment() $config = $this->deploymentConfig->get(ConfigOptionsListConstants::KEY_MODULES); if (!$config) { $messages[] = 'You cannot run this command because modules are not enabled. You can enable modules by' - . ' running the \'module:enable --all\' command.'; + . ' running the \'module:enable --all\' command.'; } return $messages; @@ -320,39 +322,40 @@ private function configureObjectManager(OutputInterface $output) { $this->objectManager->configure( [ - 'preferences' => [\Magento\Framework\App\ObjectManager\ConfigWriterInterface::class => - \Magento\Framework\App\ObjectManager\ConfigWriter\Filesystem::class, - ], \Magento\Setup\Module\Di\Compiler\Config\ModificationChain::class => [ + 'preferences' => [ + ConfigWriterInterface::class => \Magento\Framework\App\ObjectManager\ConfigWriter\Filesystem::class, + ], + \Magento\Setup\Module\Di\Compiler\Config\ModificationChain::class => [ 'arguments' => [ 'modificationsList' => [ 'BackslashTrim' => [ - 'instance' => - \Magento\Setup\Module\Di\Compiler\Config\Chain\BackslashTrim::class + 'instance' => \Magento\Setup\Module\Di\Compiler\Config\Chain\BackslashTrim::class ], 'PreferencesResolving' => [ - 'instance' => - \Magento\Setup\Module\Di\Compiler\Config\Chain\PreferencesResolving::class + 'instance' => \Magento\Setup\Module\Di\Compiler\Config\Chain\PreferencesResolving::class ], 'InterceptorSubstitution' => [ - 'instance' => - \Magento\Setup\Module\Di\Compiler\Config\Chain\InterceptorSubstitution::class + 'instance' => InterceptorSubstitutionInterface::class ], 'InterceptionPreferencesResolving' => [ 'instance' => \Magento\Setup\Module\Di\Compiler\Config\Chain\PreferencesResolving::class ], ] ] - ], \Magento\Setup\Module\Di\Code\Generator\PluginList::class => [ + ], + \Magento\Setup\Module\Di\Code\Generator\PluginList::class => [ 'arguments' => [ 'cache' => [ 'instance' => \Magento\Framework\App\Interception\Cache\CompiledConfig::class ] ] - ], \Magento\Setup\Module\Di\Code\Reader\ClassesScanner::class => [ + ], + \Magento\Setup\Module\Di\Code\Reader\ClassesScanner::class => [ 'arguments' => [ 'excludePatterns' => $this->excludedPathsList ] - ], \Magento\Setup\Module\Di\Compiler\Log\Writer\Console::class => [ + ], + \Magento\Setup\Module\Di\Compiler\Log\Writer\Console::class => [ 'arguments' => [ 'output' => $output, ] diff --git a/setup/src/Magento/Setup/Module/Di/App/Task/Operation/Interception.php b/setup/src/Magento/Setup/Module/Di/App/Task/Operation/Interception.php index 9c629998e555d..e07535f532c4a 100644 --- a/setup/src/Magento/Setup/Module/Di/App/Task/Operation/Interception.php +++ b/setup/src/Magento/Setup/Module/Di/App/Task/Operation/Interception.php @@ -5,10 +5,11 @@ */ namespace Magento\Setup\Module\Di\App\Task\Operation; +use Magento\Framework\App; +use Magento\Framework\Interception\Code\Generator\Interceptor; use Magento\Setup\Module\Di\App\Task\OperationInterface; use Magento\Setup\Module\Di\Code\Generator\InterceptionConfigurationBuilder; -use Magento\Framework\Interception\Code\Generator\Interceptor; -use Magento\Framework\App; +use Magento\Setup\Module\Di\Code\Generator\Interceptor as InterceptorGenerator; use Magento\Setup\Module\Di\Code\GeneratorFactory; use Magento\Setup\Module\Di\Code\Reader\ClassesScanner; @@ -39,11 +40,17 @@ class Interception implements OperationInterface */ private $generatorFactory; + /** + * @var string + */ + private $interceptorGeneratorClass; + /** * @param InterceptionConfigurationBuilder $interceptionConfigurationBuilder * @param App\AreaList $areaList * @param ClassesScanner $classesScanner * @param GeneratorFactory $generatorFactory + * @param string $interceptorGeneratorClass * @param array $data */ public function __construct( @@ -51,6 +58,7 @@ public function __construct( App\AreaList $areaList, ClassesScanner $classesScanner, GeneratorFactory $generatorFactory, + string $interceptorGeneratorClass = InterceptorGenerator::class, $data = [] ) { $this->interceptionConfigurationBuilder = $interceptionConfigurationBuilder; @@ -58,10 +66,11 @@ public function __construct( $this->data = $data; $this->classesScanner = $classesScanner; $this->generatorFactory = $generatorFactory; + $this->interceptorGeneratorClass = $interceptorGeneratorClass; } /** - * {@inheritdoc} + * @inheritdoc */ public function doOperation() { @@ -80,6 +89,7 @@ public function doOperation() $paths = (array)$paths; } foreach ($paths as $path) { + // phpcs:ignore Magento2.Performance.ForeachArrayMerge $classesList = array_merge($classesList, $this->classesScanner->getList($path)); } } @@ -92,7 +102,7 @@ public function doOperation() [ 'ioObject' => $generatorIo, 'generatedEntities' => [ - Interceptor::ENTITY_TYPE => \Magento\Setup\Module\Di\Code\Generator\Interceptor::class, + Interceptor::ENTITY_TYPE => $this->interceptorGeneratorClass, ] ] ); diff --git a/setup/src/Magento/Setup/Module/Di/Compiler/Config/Chain/InterceptorSubstitution.php b/setup/src/Magento/Setup/Module/Di/Compiler/Config/Chain/InterceptorSubstitution.php index 66cd7b1a595e6..9f99d5da90a84 100644 --- a/setup/src/Magento/Setup/Module/Di/Compiler/Config/Chain/InterceptorSubstitution.php +++ b/setup/src/Magento/Setup/Module/Di/Compiler/Config/Chain/InterceptorSubstitution.php @@ -7,7 +7,7 @@ use Magento\Setup\Module\Di\Compiler\Config\ModificationInterface; -class InterceptorSubstitution implements ModificationInterface +class InterceptorSubstitution implements ModificationInterface, InterceptorSubstitutionInterface { /** * Modifies input config diff --git a/setup/src/Magento/Setup/Module/Di/Compiler/Config/Chain/InterceptorSubstitutionInterface.php b/setup/src/Magento/Setup/Module/Di/Compiler/Config/Chain/InterceptorSubstitutionInterface.php new file mode 100644 index 0000000000000..52bb286a87033 --- /dev/null +++ b/setup/src/Magento/Setup/Module/Di/Compiler/Config/Chain/InterceptorSubstitutionInterface.php @@ -0,0 +1,13 @@ +