Skip to content

Instantly share code, notes, and snippets.

@mattwellss
Last active July 19, 2021 13:58
Show Gist options
  • Save mattwellss/5b133af5cf69793d0953d5fa0087e6ea to your computer and use it in GitHub Desktop.
Save mattwellss/5b133af5cf69793d0953d5fa0087e6ea to your computer and use it in GitHub Desktop.
<?php
// phpcs:ignoreFile
use bitExpert\PHPStan\Magento\Autoload\Cache\FileCacheStorage;
use Magento\Framework\App\DeploymentConfig;
use Magento\Framework\App\DeploymentConfig\Reader as DeploymentConfigReader;
use Magento\Framework\App\Filesystem\DirectoryList;
use Magento\Framework\Component\ComponentRegistrar;
use Magento\Framework\Config\File\ConfigFilePool;
use Magento\Framework\Filesystem;
use Magento\Framework\Filesystem\Driver\File as FileDriver;
use Magento\Framework\Module\Declaration\Converter\Dom as ModuleDeclarationDom;
use Magento\Framework\Module\ModuleList;
use Magento\Framework\Module\ModuleList\Loader;
use Magento\Framework\Xml\Parser as XmlParser;
use Nette\Neon\Neon;
(function (array $argv) {
require_once __DIR__ . '/ExtensionInterfaceAutoloader.php';
$configFile = '';
if (count($argv) > 0) {
foreach ($argv as $idx => $value) {
if ((strtolower($value) === '-c') && isset($argv[$idx + 1])) {
$configFile = $argv[$idx + 1];
break;
}
}
}
if (empty($configFile)) {
$currentWorkingDirectory = getcwd();
foreach (['phpstan.neon', 'phpstan.neon.dist'] as $discoverableConfigName) {
$discoverableConfigFile = $currentWorkingDirectory . DIRECTORY_SEPARATOR . $discoverableConfigName;
if (file_exists($discoverableConfigFile) && is_readable(($discoverableConfigFile))) {
$configFile = $discoverableConfigFile;
break;
}
}
}
$tmpDir = sys_get_temp_dir() . '/phpstan';
if (!empty($configFile)) {
$neonConfig = Neon::decode(file_get_contents($configFile));
if (is_array($neonConfig) && isset($neonConfig['parameters']) && isset($neonConfig['parameters']['tmpDir'])) {
$tmpDir = $neonConfig['parameters']['tmpDir'];
}
}
$componentRegistrar = new ComponentRegistrar();
$loader = new \Lfi\Codetools\PHPStan\ExtensionInterfaceAutoloader(
new ModuleList(
new DeploymentConfig(
new DeploymentConfigReader(
new DirectoryList('.'),
new Filesystem\DriverPool(),
new ConfigFilePool()
)
),
new Loader(
new ModuleDeclarationDom(),
new XmlParser(),
$componentRegistrar,
new FileDriver()
)
),
$componentRegistrar,
new \PHPStan\Cache\Cache(new FileCacheStorage($tmpDir . '/cache/PHPStan'))
);
\spl_autoload_register([$loader, 'autoload'], true, false);
})($GLOBALS['argv'] ?? []);
<?php
namespace Lfi\Codetools\PHPStan;
use Laminas\Code\Generator\DocBlock\Tag\ParamTag;
use Laminas\Code\Generator\DocBlock\Tag\ReturnTag;
use Laminas\Code\Generator\DocBlockGenerator;
use Laminas\Code\Generator\InterfaceGenerator;
use Laminas\Code\Generator\MethodGenerator;
use Magento\Framework\Api\SimpleDataObjectConverter;
use Magento\Framework\Component\ComponentRegistrar;
use Magento\Framework\Module\ModuleList;
use Symfony\Component\Finder\Finder;
class ExtensionInterfaceAutoloader
{
private $moduleList;
private $componentRegistrar;
private $cache;
private $xmlDocs;
public function __construct(
ModuleList $moduleList,
ComponentRegistrar $componentRegistrar,
\PHPStan\Cache\Cache $cache
) {
$this->moduleList = $moduleList;
$this->componentRegistrar = $componentRegistrar;
$this->cache = $cache;
}
public function autoload(string $class): void
{
if (preg_match('/ExtensionInterface$/', $class) !== 1) {
return;
}
$cachedFilename = $this->cache->load($class, '');
if (!$cachedFilename) {
try {
$this->cache->save($class, '', $this->getFileContents($class));
$cachedFilename = $this->cache->load($class, '');
} catch (\Exception $e) {
return;
}
}
// phpcs:ignore Magento2.Security.IncludeFile.FoundIncludeFile
require_once $cachedFilename;
}
/**
* Given an extension attributes interface name, generate that interface (if possible)
*/
public function getFileContents(string $interfaceName): string
{
/**
* Given a classname to autoload (such as Magento\Catalog\Api\Data\ProductExtensionInterface),
* generate the entity's interface name (like Magento\Catalog\Api\Data\ProductInterface)
* @see \Magento\Framework\Code\Generator::generateClass
* @see \Magento\Framework\Api\Code\Generator\ExtensionAttributesGenerator::__construct
*/
$sourceInterface = rtrim(substr($interfaceName, 0, -1 * strlen('ExtensionInterface')), '\\') . 'Interface';
// Magento only creates extension attribute interfaces for existing interfaces; retain that logic
if (!interface_exists($sourceInterface)) {
throw new \Exception("${sourceInterface} does not exist and has no extension interface");
}
$generator = new InterfaceGenerator();
$generator
->setName($interfaceName)
->setImplementedInterfaces([\Magento\Framework\Api\ExtensionAttributesInterface::class]);
foreach ($this->getExtensionAttributesXmlDocs() as $doc) {
$xpath = new \DOMXPath($doc);
$attrs = $xpath->query(
"//extension_attributes[@for=\"${sourceInterface}\"]/attribute",
$doc->documentElement
);
/** @var \DOMElement $attr */
foreach ($attrs as $attr) {
/**
* Generate getters and setters for each extension attribute
* @see \Magento\Framework\Api\Code\Generator\ExtensionAttributesGenerator::_getClassMethods
*/
$propertyName = SimpleDataObjectConverter::snakeCaseToCamelCase($attr->getAttribute('code'));
$type = $attr->getAttribute('type');
$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,
'null'
]
),
new ReturnTag(
'$this'
)
]
])
])
);
}
}
return "<?php\n\n" . $generator->generate();
}
/**
* Create a generator which creates DOM documents for every extension attributes XML file in enabled modules
* @return \DOMDocument[]
*/
private function getExtensionAttributesXmlDocs(): array
{
if (is_array($this->xmlDocs)) {
return $this->xmlDocs;
}
$enabledModuleDirs = array_filter(
$this->componentRegistrar->getPaths(ComponentRegistrar::MODULE),
function ($moduleName) {
return $this->moduleList->has($moduleName);
},
ARRAY_FILTER_USE_KEY
);
$finder = Finder::create()
->files()
->in(array_map(function ($dir) {
return $dir . '/etc';
}, $enabledModuleDirs))
->name('extension_attributes.xml');
$this->xmlDocs = [];
foreach ($finder as $item) {
$doc = new \DOMDocument();
$doc->loadXML($item->getContents());
$this->xmlDocs[] = $doc;
}
return $this->xmlDocs;
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment