Skip to content

Commit

Permalink
Add Composer autoloader to locate symbols
Browse files Browse the repository at this point in the history
Use Composer autoloader to check if the interface definition
for the extension attributes does in fact exist.
  • Loading branch information
shochdoerfer committed Jan 8, 2022
1 parent 56b471b commit d159afe
Show file tree
Hide file tree
Showing 9 changed files with 230 additions and 15 deletions.
4 changes: 2 additions & 2 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,8 @@
"php": "^7.2.0 || ^8.0.0",
"ext-dom": "*",
"laminas/laminas-code": "~3.3.0 || ~3.4.1 || ~3.5.1",
"symfony/finder": "^3.0 || ^4.0 || ^5.0",
"phpstan/phpstan": "^1.2.0"
"phpstan/phpstan": "^1.2.0",
"symfony/finder": "^3.0 || ^4.0 || ^5.0"
},
"conflict": {
"magento/framework": "<102.0.0"
Expand Down
2 changes: 1 addition & 1 deletion composer.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 5 additions & 0 deletions extension.neon
Original file line number Diff line number Diff line change
Expand Up @@ -75,10 +75,15 @@ services:
class: bitExpert\PHPStan\Magento\Autoload\DataProvider\ExtensionAttributeDataProvider
arguments:
magentoRoot: %magento.magentoRoot%
classLoaderProvider:
class: bitExpert\PHPStan\Magento\Autoload\DataProvider\ClassLoaderProvider
arguments:
magentoRoot: %magento.magentoRoot%
extensionInterfaceAutoloader:
class: bitExpert\PHPStan\Magento\Autoload\ExtensionInterfaceAutoloader
arguments:
cache: @autoloaderCache
attributeDataProvider: @extensionAttributeDataProvider
classLoaderProvider: @classLoaderProvider
tags:
- phpstan.magento.autoloader
3 changes: 3 additions & 0 deletions phpstan.neon
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,9 @@ parameters:
-
message: '~is not covered by backward compatibility promise.~'
path: tests/bitExpert/PHPStan/Magento/Autoload/ExtensionInterfaceAutoloaderUnitTest.php
-
message: '~Parameter #1 $argument of class ReflectionClass constructor expects class-string<UncachedExtensionInterface>|UncachedExtensionInterface, string given~'
path: tests/bitExpert/PHPStan/Magento/Autoload/ExtensionInterfaceAutoloaderUnitTest.php
-
message: '~bitExpert\\PHPStan\\Magento\\Rules\\Helper\\SampleModel::__construct\(\) does not call parent constructor~'
path: tests/bitExpert/PHPStan/Magento/Rules/Helper/SampleModel.php
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
<?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\DataProvider;

use Composer\Autoload\ClassLoader;

class ClassLoaderProvider
{
/**
* @var ClassLoader
*/
private $composer;

/**
* ClassLoaderProvider constructor.
*
* @param string $magentoRoot
*/
public function __construct(string $magentoRoot)
{
$this->composer = new ClassLoader($magentoRoot.'/vendor');
$autoloadFile = $magentoRoot.'/vendor/composer/autoload_namespaces.php';
if (is_file($autoloadFile)) {
$map = require $autoloadFile;
foreach ($map as $namespace => $path) {
$this->composer->set($namespace, $path);
}
}

$autoloadFile = $magentoRoot.'/vendor/composer/autoload_psr4.php';
if (is_file($autoloadFile)) {
$map = require $autoloadFile;
foreach ($map as $namespace => $path) {
$this->composer->setPsr4($namespace, $path);
}
}

$autoloadFile = $magentoRoot.'/vendor/composer/autoload_classmap.php';
if (is_file($autoloadFile)) {
$classMap = require $autoloadFile;
if (is_array($classMap)) {
$this->composer->addClassMap($classMap);
}
}
}

/**
* Check if the given class/interface/tait exists in the defined scope.
*
* @param string $classyConstructName
* @return bool
*/
public function exists(string $classyConstructName): bool
{
return $this->composer->findFile($classyConstructName) !== false;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@

namespace bitExpert\PHPStan\Magento\Autoload;

use bitExpert\PHPStan\Magento\Autoload\DataProvider\ClassLoaderProvider;
use bitExpert\PHPStan\Magento\Autoload\DataProvider\ExtensionAttributeDataProvider;
use Laminas\Code\Generator\DocBlock\Tag\ParamTag;
use Laminas\Code\Generator\DocBlock\Tag\ReturnTag;
Expand All @@ -30,17 +31,26 @@ class ExtensionInterfaceAutoloader implements Autoloader
* @var ExtensionAttributeDataProvider
*/
private $attributeDataProvider;
/**
* @var ClassLoaderProvider
*/
private $classLoaderProvider;

/**
* ExtensionInterfaceAutoloader constructor.
*
* @param Cache $cache
* @param ExtensionAttributeDataProvider $attributeDataProvider
* @param ClassLoaderProvider $classLoaderProvider
*/
public function __construct(Cache $cache, ExtensionAttributeDataProvider $attributeDataProvider)
{
public function __construct(
Cache $cache,
ExtensionAttributeDataProvider $attributeDataProvider,
ClassLoaderProvider $classLoaderProvider
) {
$this->cache = $cache;
$this->attributeDataProvider = $attributeDataProvider;
$this->classLoaderProvider = $classLoaderProvider;
}

public function autoload(string $class): void
Expand Down Expand Up @@ -76,6 +86,11 @@ public function getFileContents(string $interfaceName): string
*/
$sourceInterface = rtrim(substr($interfaceName, 0, -1 * strlen('ExtensionInterface')), '\\') . 'Interface';

// Magento only creates extension attribute interfaces for existing interfaces; retain that logic
if (!$this->classLoaderProvider->exists($sourceInterface)) {
throw new \InvalidArgumentException("${sourceInterface} does not exist and has no extension interface");
}

$generator = new InterfaceGenerator();
$generator
->setName($interfaceName)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
<?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\DataProvider;

use PHPUnit\Framework\TestCase;

class ClassLoaderProviderUnitTest extends TestCase
{
/**
* @var ClassLoaderProvider
*/
private $dataprovider;

protected function setUp(): void
{
$this->dataprovider = new ClassLoaderProvider(__DIR__ . '/../../../../../../');
}

/**
* @test
*/
public function returnsTrueForClassesFound(): void
{
static::assertTrue($this->dataprovider->exists(ClassLoaderProviderUnitTest::class));
}

/**
* @test
*/
public function returnsFalseWhenClassNotFound(): void
{
static::assertFalse($this->dataprovider->exists('SomeOtherClass'));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,39 +2,50 @@

namespace bitExpert\PHPStan\Magento\Autoload;

use bitExpert\PHPStan\Magento\Autoload\Cache\FileCacheStorage;
use bitExpert\PHPStan\Magento\Autoload\DataProvider\ClassLoaderProvider;
use bitExpert\PHPStan\Magento\Autoload\DataProvider\ExtensionAttributeDataProvider;
use org\bovigo\vfs\vfsStream;
use PHPStan\Cache\Cache;
use PHPStan\Cache\CacheStorage;
use PHPUnit\Framework\TestCase;

class ExtensionInterfaceAutoloaderUnitTest extends TestCase
{
/**
* @var CacheStorage|\PHPUnit\Framework\MockObject\MockObject
* @var Cache|\PHPUnit\Framework\MockObject\MockObject
*/
private $storage;
private $cache;
/**
* @var ExtensionAttributeDataProvider|\PHPUnit\Framework\MockObject\MockObject
*/
private $dataProvider;
private $extAttrDataProvider;
/**
* @var ClassLoaderProvider|\PHPUnit\Framework\MockObject\MockObject
*/
private $classyDataProvider;
/**
* @var ExtensionInterfaceAutoloader
*/
private $autoloader;

protected function setUp(): void
{
$this->storage = $this->createMock(CacheStorage::class);
$this->dataProvider = $this->createMock(ExtensionAttributeDataProvider::class);
$this->autoloader = new ExtensionInterfaceAutoloader(new Cache($this->storage), $this->dataProvider);
$this->cache = $this->createMock(Cache::class);
$this->extAttrDataProvider = $this->createMock(ExtensionAttributeDataProvider::class);
$this->classyDataProvider = $this->createMock(ClassLoaderProvider::class);
$this->autoloader = new ExtensionInterfaceAutoloader(
$this->cache,
$this->extAttrDataProvider,
$this->classyDataProvider
);
}

/**
* @test
*/
public function autoloaderIgnoresClassesWithoutExtensionInterfacePostfix(): void
{
$this->storage->expects(self::never())
$this->cache->expects(self::never())
->method('load');

$this->autoloader->autoload('SomeClass');
Expand All @@ -45,12 +56,79 @@ public function autoloaderIgnoresClassesWithoutExtensionInterfacePostfix(): void
*/
public function autoloaderUsesCachedFileWhenFound(): void
{
$this->storage->expects(self::once())
$this->cache->expects(self::once())
->method('load')
->willReturn(__DIR__ . '/HelperExtensionInterface.php');

$this->cache->expects(self::never())
->method('save');

$this->autoloader->autoload(HelperExtensionInterface::class);

self::assertTrue(interface_exists(HelperExtensionInterface::class, false));
}

/**
* @test
*/
public function autoloadDoesNotGenerateInterfaceWhenNoAttributesExist(): void
{
$interfaceName = 'NonExistentExtensionInterface';

$this->cache->expects(self::once())
->method('load')
->willReturn(null);

$this->classyDataProvider->expects(self::once())
->method('exists')
->willReturn(false);

$this->autoloader->autoload($interfaceName);
static::assertFalse(interface_exists($interfaceName));
}

/**
* @test
*/
public function autoloadGeneratesInterfaceWhenNotCached(): void
{
$interfaceName = 'UncachedExtensionInterface';

$root = vfsStream::setup('test');
$cache = new Cache(new FileCacheStorage($root->url() . '/tmp/cache/PHPStan'));
$autoloader = new ExtensionInterfaceAutoloader($cache, $this->extAttrDataProvider, $this->classyDataProvider);

$this->classyDataProvider->expects(self::once())
->method('exists')
->willReturn(true);

$this->extAttrDataProvider->expects(self::once())
->method('getAttributesForInterface')
->willReturn(['attr' => 'string']);

$autoloader->autoload($interfaceName);
static::assertTrue(interface_exists($interfaceName));
$interfaceReflection = new \ReflectionClass($interfaceName);
try {
$getAttrReflection = $interfaceReflection->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 interface');
}

try {
$setAttrReflection = $interfaceReflection->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 interface');
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@

namespace bitExpert\PHPStan\Magento\Autoload;

use bitExpert\PHPStan\Magento\Autoload\DataProvider\ClassLoaderProvider;
use bitExpert\PHPStan\Magento\Autoload\DataProvider\ExtensionAttributeDataProvider;
use PHPStan\Cache\Cache;
use PHPUnit\Framework\TestCase;
Expand Down Expand Up @@ -50,7 +51,11 @@ public function provideAutoloaders(): array
[new MockAutoloader()],
[new ProxyAutoloader($cache)],
[new TestFrameworkAutoloader()],
[new ExtensionInterfaceAutoloader($cache, new ExtensionAttributeDataProvider('.'))]
[new ExtensionInterfaceAutoloader(
$cache,
new ExtensionAttributeDataProvider(__DIR__),
new ClassLoaderProvider(__DIR__)
)]
];
}
}

0 comments on commit d159afe

Please sign in to comment.