Skip to content

Instantly share code, notes, and snippets.

@peterjaap
Created April 15, 2025 14:31
Show Gist options
  • Save peterjaap/4ef553ba558caf3b0456401051390d7a to your computer and use it in GitHub Desktop.
Save peterjaap/4ef553ba558caf3b0456401051390d7a to your computer and use it in GitHub Desktop.
PHP script to detect potentially unused Magento 2 extensions
<?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