-
Notifications
You must be signed in to change notification settings - Fork 25
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #191 from shochdoerfer/feature/extension_attributes
Add autoloader for extension classes
- Loading branch information
Showing
6 changed files
with
273 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
125 changes: 125 additions & 0 deletions
125
src/bitExpert/PHPStan/Magento/Autoload/ExtensionAutoloader.php
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,125 @@ | ||
<?php | ||
|
||
/* | ||
* This file is part of the phpstan-magento package. | ||
* | ||
* (c) bitExpert AG | ||
* | ||
* For the full copyright and license information, please view the LICENSE | ||
* file that was distributed with this source code. | ||
*/ | ||
declare(strict_types=1); | ||
|
||
namespace bitExpert\PHPStan\Magento\Autoload; | ||
|
||
use bitExpert\PHPStan\Magento\Autoload\DataProvider\ExtensionAttributeDataProvider; | ||
use Laminas\Code\Generator\ClassGenerator; | ||
use Laminas\Code\Generator\DocBlock\Tag\ParamTag; | ||
use Laminas\Code\Generator\DocBlock\Tag\ReturnTag; | ||
use Laminas\Code\Generator\DocBlockGenerator; | ||
use Laminas\Code\Generator\MethodGenerator; | ||
use PHPStan\Cache\Cache; | ||
|
||
class ExtensionAutoloader implements Autoloader | ||
{ | ||
/** | ||
* @var Cache | ||
*/ | ||
private $cache; | ||
/** | ||
* @var ExtensionAttributeDataProvider | ||
*/ | ||
private $attributeDataProvider; | ||
|
||
/** | ||
* ExtensionAutoloader constructor. | ||
* | ||
* @param Cache $cache | ||
* @param ExtensionAttributeDataProvider $attributeDataProvider | ||
*/ | ||
public function __construct( | ||
Cache $cache, | ||
ExtensionAttributeDataProvider $attributeDataProvider | ||
) { | ||
$this->cache = $cache; | ||
$this->attributeDataProvider = $attributeDataProvider; | ||
} | ||
|
||
public function autoload(string $class): void | ||
{ | ||
if (preg_match('#Extension$#', $class) !== 1) { | ||
return; | ||
} | ||
|
||
$cachedFilename = $this->cache->load($class, ''); | ||
if ($cachedFilename === null) { | ||
try { | ||
$this->cache->save($class, '', $this->getFileContents($class)); | ||
$cachedFilename = $this->cache->load($class, ''); | ||
} catch (\Exception $e) { | ||
return; | ||
} | ||
} | ||
|
||
require_once($cachedFilename); | ||
} | ||
|
||
/** | ||
* Given an extension attributes interface name, generate that interface (if possible) | ||
*/ | ||
public function getFileContents(string $className): string | ||
{ | ||
$sourceInterface = rtrim(substr($className, 0, -1 * strlen('Extension')), '\\') . 'ExtensionInterface'; | ||
$attrInterface = rtrim(substr($sourceInterface, 0, -1 * strlen('ExtensionInterface')), '\\') . 'Interface'; | ||
|
||
$generator = new ClassGenerator(); | ||
$generator | ||
->setName($className) | ||
->setExtendedClass('\Magento\Framework\Api\AbstractSimpleObject') | ||
->setImplementedInterfaces([$sourceInterface]); | ||
|
||
$attrs = $this->attributeDataProvider->getAttributesForInterface($attrInterface); | ||
foreach ($attrs as $propertyName => $type) { | ||
/** | ||
* Generate getters and setters for each extension attribute | ||
* | ||
* @see \Magento\Framework\Api\Code\Generator\ExtensionAttributesGenerator::_getClassMethods | ||
*/ | ||
|
||
$generator->addMethodFromGenerator( | ||
MethodGenerator::fromArray([ | ||
'name' => 'get' . ucfirst($propertyName), | ||
'docblock' => DocBlockGenerator::fromArray([ | ||
'tags' => [ | ||
new ReturnTag([$type, 'null']), | ||
], | ||
]), | ||
]) | ||
); | ||
$generator->addMethodFromGenerator( | ||
MethodGenerator::fromArray([ | ||
'name' => 'set' . ucfirst($propertyName), | ||
'parameters' => [$propertyName], | ||
'docblock' => DocBlockGenerator::fromArray([ | ||
'tags' => [ | ||
new ParamTag($propertyName, [$type]), | ||
new ReturnTag('$this') | ||
] | ||
]) | ||
]) | ||
); | ||
} | ||
|
||
return "<?php\n\n" . $generator->generate(); | ||
} | ||
|
||
public function register(): void | ||
{ | ||
\spl_autoload_register([$this, 'autoload'], true, false); | ||
} | ||
|
||
public function unregister(): void | ||
{ | ||
\spl_autoload_unregister([$this, 'autoload']); | ||
} | ||
} |
107 changes: 107 additions & 0 deletions
107
tests/bitExpert/PHPStan/Magento/Autoload/ExtensionAutoloaderUnitTest.php
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,107 @@ | ||
<?php | ||
|
||
namespace bitExpert\PHPStan\Magento\Autoload; | ||
|
||
use bitExpert\PHPStan\Magento\Autoload\Cache\FileCacheStorage; | ||
use bitExpert\PHPStan\Magento\Autoload\DataProvider\ExtensionAttributeDataProvider; | ||
use org\bovigo\vfs\vfsStream; | ||
use PHPStan\Cache\Cache; | ||
use PHPUnit\Framework\TestCase; | ||
|
||
class ExtensionAutoloaderUnitTest extends TestCase | ||
{ | ||
/** | ||
* @var Cache|\PHPUnit\Framework\MockObject\MockObject | ||
*/ | ||
private $cache; | ||
/** | ||
* @var ExtensionAttributeDataProvider|\PHPUnit\Framework\MockObject\MockObject | ||
*/ | ||
private $extAttrDataProvider; | ||
/** | ||
* @var ExtensionAutoloader | ||
*/ | ||
private $autoloader; | ||
|
||
protected function setUp(): void | ||
{ | ||
$this->cache = $this->createMock(Cache::class); | ||
$this->extAttrDataProvider = $this->createMock(ExtensionAttributeDataProvider::class); | ||
$this->autoloader = new ExtensionAutoloader( | ||
$this->cache, | ||
$this->extAttrDataProvider | ||
); | ||
} | ||
|
||
/** | ||
* @test | ||
*/ | ||
public function autoloaderIgnoresClassesWithoutExtensionInterfacePostfix(): void | ||
{ | ||
$this->cache->expects(self::never()) | ||
->method('load'); | ||
|
||
$this->autoloader->autoload('SomeClass'); | ||
} | ||
|
||
/** | ||
* @test | ||
*/ | ||
public function autoloaderUsesCachedFileWhenFound(): void | ||
{ | ||
$this->cache->expects(self::once()) | ||
->method('load') | ||
->willReturn(__DIR__ . '/HelperExtension.php'); | ||
|
||
$this->cache->expects(self::never()) | ||
->method('save'); | ||
|
||
$this->autoloader->autoload(HelperExtension::class); | ||
|
||
self::assertTrue(class_exists(HelperExtension::class, false)); | ||
} | ||
|
||
/** | ||
* @test | ||
*/ | ||
public function autoloadGeneratesInterfaceWhenNotCached(): void | ||
{ | ||
$className = 'MyUncachedExtension'; | ||
// since the generated class implements an interface, we need to make it available here, otherwise | ||
// the autoloader will fail with an exception that the interface can't be found! | ||
class_alias(HelperExtensionInterface::class, 'MyUncachedExtensionInterface'); | ||
|
||
$root = vfsStream::setup('test'); | ||
$cache = new Cache(new FileCacheStorage($root->url() . '/tmp/cache/PHPStan')); | ||
$autoloader = new ExtensionAutoloader($cache, $this->extAttrDataProvider); | ||
|
||
$this->extAttrDataProvider->expects(self::once()) | ||
->method('getAttributesForInterface') | ||
->willReturn(['attr' => 'string']); | ||
|
||
$autoloader->autoload($className); | ||
static::assertTrue(class_exists($className)); | ||
$classReflection = new \ReflectionClass($className); | ||
try { | ||
$getAttrReflection = $classReflection->getMethod('getAttr'); | ||
$docComment = $getAttrReflection->getDocComment(); | ||
if (!is_string($docComment)) { | ||
throw new \ReflectionException(); | ||
} | ||
static::assertStringContainsString('@return string|null', $docComment); | ||
} catch (\ReflectionException $e) { | ||
static::fail('Could not find expected method getAttr on generated class'); | ||
} | ||
|
||
try { | ||
$setAttrReflection = $classReflection->getMethod('setAttr'); | ||
$docComment = $setAttrReflection->getDocComment(); | ||
if (!is_string($docComment)) { | ||
throw new \ReflectionException(); | ||
} | ||
static::assertStringContainsString('@param string $attr', $docComment); | ||
} catch (\ReflectionException $e) { | ||
static::fail('Could not find expected generated method setAttr on generated class'); | ||
} | ||
} | ||
} |
20 changes: 20 additions & 0 deletions
20
tests/bitExpert/PHPStan/Magento/Autoload/HelperExtension.php
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,20 @@ | ||
<?php | ||
|
||
/* | ||
* This file is part of the phpstan-magento package. | ||
* | ||
* (c) bitExpert AG | ||
* | ||
* For the full copyright and license information, please view the LICENSE | ||
* file that was distributed with this source code. | ||
*/ | ||
declare(strict_types=1); | ||
|
||
namespace bitExpert\PHPStan\Magento\Autoload; | ||
|
||
/** | ||
* Dummy attribute extension interface that can be loaded via the Autoloader in the test cases. | ||
*/ | ||
class HelperExtension extends \Magento\Framework\Api\AbstractSimpleObject implements HelperExtensionInterface | ||
{ | ||
} |