Skip to content

Commit effb7c0

Browse files
authored
ENGCOM-4104: #20773: Do not throw exception during autoload #20950
2 parents d5cc340 + 15a1f37 commit effb7c0

File tree

2 files changed

+151
-9
lines changed

2 files changed

+151
-9
lines changed
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
<?php declare(strict_types=1);
2+
/**
3+
* Copyright © Magento, Inc. All rights reserved.
4+
* See COPYING.txt for license details.
5+
*/
6+
7+
namespace Magento\Framework\Code\Generator;
8+
9+
use Magento\Framework\Code\Generator;
10+
use Magento\Framework\Logger\Monolog as MagentoMonologLogger;
11+
use Magento\TestFramework\ObjectManager;
12+
use PHPUnit\Framework\TestCase;
13+
use PHPUnit_Framework_MockObject_MockObject as MockObject;
14+
use Psr\Log\LoggerInterface;
15+
16+
class AutoloaderTest extends TestCase
17+
{
18+
/**
19+
* This method exists to fix the wrong return type hint on \Magento\Framework\App\ObjectManager::getInstance.
20+
* This way the IDE knows it's dealing with an instance of \Magento\TestFramework\ObjectManager and
21+
* not \Magento\Framework\App\ObjectManager. The former has the method addSharedInstance, the latter does not.
22+
*
23+
* @return ObjectManager|\Magento\Framework\App\ObjectManager
24+
* @SuppressWarnings(PHPMD.StaticAccess)
25+
*/
26+
private function getTestFrameworkObjectManager()
27+
{
28+
return ObjectManager::getInstance();
29+
}
30+
31+
/**
32+
* @before
33+
*/
34+
public function setupLoggerTestDouble(): void
35+
{
36+
$loggerTestDouble = $this->createMock(LoggerInterface::class);
37+
$this->getTestFrameworkObjectManager()->addSharedInstance($loggerTestDouble, MagentoMonologLogger::class);
38+
}
39+
40+
/**
41+
* @after
42+
*/
43+
public function removeLoggerTestDouble(): void
44+
{
45+
$this->getTestFrameworkObjectManager()->removeSharedInstance(MagentoMonologLogger::class);
46+
}
47+
48+
/**
49+
* @param \RuntimeException $testException
50+
* @return Generator|MockObject
51+
*/
52+
private function createExceptionThrowingGeneratorTestDouble(\RuntimeException $testException)
53+
{
54+
/** @var Generator|MockObject $generatorStub */
55+
$generatorStub = $this->createMock(Generator::class);
56+
$generatorStub->method('generateClass')->willThrowException($testException);
57+
58+
return $generatorStub;
59+
}
60+
61+
public function testLogsExceptionDuringGeneration(): void
62+
{
63+
$exceptionMessage = 'Test exception thrown during generation';
64+
$testException = new \RuntimeException($exceptionMessage);
65+
66+
$loggerMock = ObjectManager::getInstance()->get(LoggerInterface::class);
67+
$loggerMock->expects($this->once())->method('debug')->with($exceptionMessage, ['exception' => $testException]);
68+
69+
$autoloader = new Autoloader($this->createExceptionThrowingGeneratorTestDouble($testException));
70+
$this->assertNull($autoloader->load(NonExistingClassName::class));
71+
}
72+
73+
public function testFiltersDuplicateExceptionMessages(): void
74+
{
75+
$exceptionMessage = 'Test exception thrown during generation';
76+
$testException = new \RuntimeException($exceptionMessage);
77+
78+
$loggerMock = ObjectManager::getInstance()->get(LoggerInterface::class);
79+
$loggerMock->expects($this->once())->method('debug')->with($exceptionMessage, ['exception' => $testException]);
80+
81+
$autoloader = new Autoloader($this->createExceptionThrowingGeneratorTestDouble($testException));
82+
$autoloader->load(OneNonExistingClassName::class);
83+
$autoloader->load(AnotherNonExistingClassName::class);
84+
}
85+
}

lib/internal/Magento/Framework/Code/Generator/Autoloader.php

Lines changed: 66 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -3,37 +3,94 @@
33
* Copyright © Magento, Inc. All rights reserved.
44
* See COPYING.txt for license details.
55
*/
6+
67
namespace Magento\Framework\Code\Generator;
78

9+
use Magento\Framework\App\ObjectManager;
810
use Magento\Framework\Code\Generator;
11+
use Psr\Log\LoggerInterface;
912

13+
/**
14+
* Class loader and generator.
15+
*/
1016
class Autoloader
1117
{
1218
/**
13-
* @var \Magento\Framework\Code\Generator
19+
* @var Generator
1420
*/
1521
protected $_generator;
1622

1723
/**
18-
* @param \Magento\Framework\Code\Generator $generator
24+
* Enables guarding against spamming the debug log with duplicate messages, as
25+
* the generation exception will be thrown multiple times within a single request.
26+
*
27+
* @var string
28+
*/
29+
private $lastGenerationErrorMessage;
30+
31+
/**
32+
* @param Generator $generator
1933
*/
20-
public function __construct(
21-
\Magento\Framework\Code\Generator $generator
22-
) {
34+
public function __construct(Generator $generator)
35+
{
2336
$this->_generator = $generator;
2437
}
2538

2639
/**
2740
* Load specified class name and generate it if necessary
2841
*
42+
* According to PSR-4 section 2.4 an autoloader MUST NOT throw an exception and SHOULD NOT return a value.
43+
*
44+
* @see https://www.php-fig.org/psr/psr-4/
45+
*
2946
* @param string $className
30-
* @return bool True if class was loaded
47+
* @return void
3148
*/
3249
public function load($className)
3350
{
34-
if (!class_exists($className)) {
35-
return Generator::GENERATION_ERROR != $this->_generator->generateClass($className);
51+
if (! class_exists($className)) {
52+
try {
53+
$this->_generator->generateClass($className);
54+
} catch (\Exception $exception) {
55+
$this->tryToLogExceptionMessageIfNotDuplicate($exception);
56+
}
57+
}
58+
}
59+
60+
/**
61+
* Log exception.
62+
*
63+
* @param \Exception $exception
64+
*/
65+
private function tryToLogExceptionMessageIfNotDuplicate(\Exception $exception): void
66+
{
67+
if ($this->lastGenerationErrorMessage !== $exception->getMessage()) {
68+
$this->lastGenerationErrorMessage = $exception->getMessage();
69+
$this->tryToLogException($exception);
70+
}
71+
}
72+
73+
/**
74+
* Try to capture the exception message.
75+
*
76+
* The Autoloader is instantiated before the ObjectManager, so the LoggerInterface can not be injected.
77+
* The Logger is instantiated in the try/catch block because ObjectManager might still not be initialized.
78+
* In that case the exception message can not be captured.
79+
*
80+
* The debug level is used for logging in case class generation fails for a common class, but a custom
81+
* autoloader is used later in the stack. A more severe log level would fill the logs with messages on production.
82+
* The exception message now can be accessed in developer mode if debug logging is enabled.
83+
*
84+
* @param \Exception $exception
85+
* @return void
86+
*/
87+
private function tryToLogException(\Exception $exception): void
88+
{
89+
try {
90+
$logger = ObjectManager::getInstance()->get(LoggerInterface::class);
91+
$logger->debug($exception->getMessage(), ['exception' => $exception]);
92+
} catch (\Exception $ignoreThisException) {
93+
// Do not take an action here, since the original exception might have been caused by logger
3694
}
37-
return true;
3895
}
3996
}

0 commit comments

Comments
 (0)