Created
April 15, 2025 14:31
-
-
Save peterjaap/4ef553ba558caf3b0456401051390d7a to your computer and use it in GitHub Desktop.
PHP script to detect potentially unused Magento 2 extensions
This file contains hidden or 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
<?php | |
require __DIR__ . '/app/bootstrap.php'; | |
use Magento\Framework\App\Bootstrap; | |
use Magento\Framework\App\ResourceConnection; | |
use Magento\Framework\Module\ModuleListInterface; | |
use Magento\Framework\Module\Dir\Reader as ModuleDirReader; | |
$bootstrap = Bootstrap::create(BP, $_SERVER); | |
$obj = $bootstrap->getObjectManager(); | |
$appState = $obj->get(\Magento\Framework\App\State::class); | |
$appState->setAreaCode('adminhtml'); | |
$moduleList = $obj->get(ModuleListInterface::class); | |
$moduleDirReader = $obj->get(ModuleDirReader::class); | |
$resource = $obj->get(ResourceConnection::class); | |
$db = $resource->getConnection(); | |
$scopeConfig = $obj->get(\Magento\Framework\App\Config\ScopeConfigInterface::class); | |
function logInfo(string $msg): void { | |
echo "[INFO] $msg\n"; | |
} | |
function logWarn(string $msg): void { | |
echo "[WARN] $msg\n"; | |
} | |
function getModuleDeclaredTables(string $modulePath): array { | |
$tables = []; | |
$dbSchema = $modulePath . '/etc/db_schema.xml'; | |
if (file_exists($dbSchema)) { | |
logInfo("Parsing: $dbSchema"); | |
$xml = simplexml_load_file($dbSchema); | |
foreach ($xml->table as $table) { | |
$tables[] = (string) $table['name']; | |
} | |
} | |
$legacyInstallSchema = glob($modulePath . '/Setup/InstallSchema.php'); | |
foreach ($legacyInstallSchema as $file) { | |
logInfo("Scanning legacy InstallSchema: $file"); | |
$contents = file_get_contents($file); | |
preg_match_all('/->createTable\(\$setup->getTable\([\'"]([^\'"]+)[\'"]\)\)/', $contents, $matches); | |
$tables = array_merge($tables, $matches[1]); | |
} | |
return array_unique($tables); | |
} | |
echo "=== Unused Extensions Detection ===\n\n"; | |
$config = include BP . '/app/etc/config.php'; | |
$unusedModules = []; | |
foreach ($moduleList->getAll() as $moduleName => $info) { | |
logInfo("Processing module: $moduleName"); | |
$modulePath = $moduleDirReader->getModuleDir('', $moduleName); | |
$declaredTables = getModuleDeclaredTables($modulePath); | |
$reason = []; | |
// 1. Tables declared but all empty or missing | |
$tablesEmpty = true; | |
foreach ($declaredTables as $table) { | |
if (!$db->isTableExists($table)) { | |
logWarn("Table '$table' does not exist"); | |
continue; | |
} | |
$rowCount = (int) $db->fetchOne("SELECT COUNT(*) FROM `$table`"); | |
logInfo("Table '$table' has $rowCount rows"); | |
if ($rowCount > 0) { | |
$tablesEmpty = false; | |
break; | |
} | |
} | |
if ($declaredTables && $tablesEmpty) { | |
$reason[] = 'All declared tables empty or missing'; | |
} | |
// 2. EAV attributes declared but unused | |
$eavTableSuffixes = ['varchar', 'int', 'text', 'decimal', 'datetime']; | |
$attributeIds = []; | |
foreach ($eavTableSuffixes as $suffix) { | |
$table = "catalog_product_entity_$suffix"; | |
if (!$db->isTableExists($table)) continue; | |
$ids = $db->fetchCol("SELECT DISTINCT attribute_id FROM $table"); | |
$attributeIds = array_merge($attributeIds, $ids); | |
} | |
$attributeIds = array_unique($attributeIds); | |
$moduleAttributes = $db->fetchCol( | |
"SELECT attribute_id FROM eav_attribute WHERE source_model LIKE ?", ["%$moduleName%"] | |
); | |
$unusedAttrs = array_diff($moduleAttributes, $attributeIds); | |
if ($moduleAttributes && count($unusedAttrs) === count($moduleAttributes)) { | |
$reason[] = 'All declared EAV attributes unused'; | |
} | |
// 3. Module is disabled in app/etc/config.php | |
$isDisabled = !isset($config['modules'][$moduleName]) || (int) $config['modules'][$moduleName] === 0; | |
if ($isDisabled) { | |
$reason[] = 'Module is disabled in app/etc/config.php'; | |
} | |
// 4. Optional: frontend output disabled | |
$configDisabled = $scopeConfig->isSetFlag("advanced/modules_disable_output/{$moduleName}"); | |
if ($configDisabled) { | |
$reason[] = 'Frontend output is disabled via config'; | |
} | |
// Add to summary if anything is worth flagging | |
if (!empty($reason)) { | |
$unusedModules[] = [ | |
'name' => $moduleName, | |
'reason' => implode('; ', $reason), | |
]; | |
} | |
} | |
// Output summary table | |
if (!empty($unusedModules)) { | |
echo "\n=== Summary: Possibly Unused Extensions ===\n\n"; | |
$maxLen = max(array_map(fn($m) => strlen($m['name']), $unusedModules)); | |
$col1 = str_pad('Extension', $maxLen + 2); | |
echo "$col1 | Reason\n"; | |
echo str_repeat('-', strlen($col1) + 40) . "\n"; | |
foreach ($unusedModules as $mod) { | |
$name = str_pad($mod['name'], $maxLen + 2); | |
echo "$name | {$mod['reason']}\n"; | |
} | |
} else { | |
echo "\nNo apparently unused extensions found.\n"; | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment