PHP Classes

File: app/modules/namespace-browser.js

Recommend this page to a friend!
  Packages of DeGraciaMathieu   PHP Dep Dashboard   app/modules/namespace-browser.js   Download  
File: app/modules/namespace-browser.js
Role: Auxiliary data
Content type: text/plain
Description: Auxiliary data
Class: PHP Dep Dashboard
Visualize PHP class dependencies in a Web page
Author: By
Last change:
Date: 10 hours ago
Size: 11,117 bytes
 

Contents

Class file image Download
import { emit, state } from './state.js'; let currentScope = []; // array of namespace segments, e.g. ['App', 'Services'] let viewMode = 'folders'; // 'folders' | 'classes' let nsData = null; export function getViewMode() { return viewMode; } export function setViewMode(mode) { viewMode = mode; renderBreadcrumb(); emit('selection:cleared'); emit('namespace:rebuild'); } /** * Build Cytoscape elements for the given scope level. * * At scope [] (root): one node per top-level namespace segment. * At scope ['App']: one node per child of App (sub-namespace folders or leaf classes). * Edges are aggregated between nodes at the current scope level. */ export function buildNamespaceElementsAtScope(data, scope) { const SEP = '\\'; const nodes = []; const edges = []; // Degree for class node sizing const degree = new Map(); for (const e of data.edges) { degree.set(e.source, (degree.get(e.source) || 0) + 1); degree.set(e.target, (degree.get(e.target) || 0) + 1); } // Only internal classes const allClasses = [...data.classes.values()].filter((c) => !c.external); // Group classes by their child segment at this scope level // Map: segment ? { isFolder: bool, fqcns: Set<string> } const childInfo = new Map(); for (const cls of allClasses) { const parts = cls.fqcn.split(SEP); // Skip if not under current scope if (scope.length > 0) { if (parts.length <= scope.length) continue; let match = true; for (let i = 0; i < scope.length; i++) { if (parts[i] !== scope[i]) { match = false; break; } } if (!match) continue; } const segment = parts[scope.length]; if (segment === undefined) continue; if (!childInfo.has(segment)) { childInfo.set(segment, { isFolder: false, fqcns: new Set() }); } childInfo.get(segment).fqcns.add(cls.fqcn); // If there are more parts, this segment is a namespace folder if (parts.length > scope.length + 1) { childInfo.get(segment).isFolder = true; } } // Cycle detection: which FQCNs are in cycles? const cycleSet = new Set((state.cycles || []).flat()); // Build fqcn ? nodeId mapping for edge aggregation const fqcnToNodeId = new Map(); const prefix = scope.length > 0 ? scope.join(SEP) + SEP : ''; for (const [segment, info] of childInfo) { const nsPath = prefix + segment; if (info.isFolder) { const nodeId = 'ns::' + nsPath; for (const fqcn of info.fqcns) fqcnToNodeId.set(fqcn, nodeId); const hasCycle = [...info.fqcns].some((fqcn) => cycleSet.has(fqcn)); nodes.push({ data: { id: nodeId, label: segment + '\n' + info.fqcns.size + (info.fqcns.size === 1 ? ' class' : ' classes'), nodeType: 'namespace', nsPath, classCount: info.fqcns.size, hasCycle, }, }); } else { const fqcn = [...info.fqcns][0]; const cls = data.classes.get(fqcn); fqcnToNodeId.set(fqcn, fqcn); nodes.push({ data: { id: fqcn, label: fqcn.split(SEP).pop(), fullLabel: fqcn, type: cls.type, nodeType: 'class', external: false, namespace: cls.namespace, file: cls.file, line: cls.line, depCount: cls.dependencies ? cls.dependencies.length : 0, dependantCount: cls.dependants ? cls.dependants.length : 0, degree: degree.get(fqcn) || 0, instability: cls.instability, fanOut: cls.fanOut, fanIn: cls.fanIn, }, }); } } // Build aggregated edges const renderedNodeIds = new Set(nodes.map((n) => n.data.id)); const CONFIDENCE_RANK = { certain: 4, high: 3, medium: 2, low: 1 }; const edgeMap = new Map(); for (const e of data.edges) { const srcId = fqcnToNodeId.get(e.source); const tgtId = fqcnToNodeId.get(e.target); if (!srcId || !tgtId || srcId === tgtId) continue; if (!renderedNodeIds.has(srcId) || !renderedNodeIds.has(tgtId)) continue; const key = srcId + '\u2192' + tgtId; if (!edgeMap.has(key)) edgeMap.set(key, { source: srcId, target: tgtId, entries: [] }); edgeMap.get(key).entries.push(e); } // Compute namespace-level instability from aggregated edges const nsFanOut = new Map(); const nsFanIn = new Map(); for (const { source, target } of edgeMap.values()) { nsFanOut.set(source, (nsFanOut.get(source) || 0) + 1); nsFanIn.set(target, (nsFanIn.get(target) || 0) + 1); } for (const node of nodes) { if (node.data.nodeType !== 'namespace') continue; const id = node.data.id; const ce = nsFanOut.get(id) || 0; const ca = nsFanIn.get(id) || 0; const instability = (ce + ca === 0) ? null : parseFloat((ce / (ce + ca)).toFixed(2)); node.data.instability = instability; node.data.fanOut = ce; node.data.fanIn = ca; } let ei = 0; for (const group of edgeMap.values()) { const { source, target, entries } = group; const best = entries.reduce((a, b) => (CONFIDENCE_RANK[b.confidence] || 0) > (CONFIDENCE_RANK[a.confidence] || 0) ? b : a ); edges.push({ data: { id: 'e' + ei++, source, target, weight: entries.length, confidence: best.confidence, edgeType: entries.map((e) => e.type).join(', '), entries, }, }); } return [...nodes, ...edges]; } /** * Build Cytoscape elements showing all individual classes at the given scope, * grouped into namespace compound-node containers for visual highlighting. */ export function buildClassElementsAtScope(data, scope) { const SEP = '\\'; const nodes = []; const edges = []; const degree = new Map(); for (const e of data.edges) { degree.set(e.source, (degree.get(e.source) || 0) + 1); degree.set(e.target, (degree.get(e.target) || 0) + 1); } const allClasses = [...data.classes.values()].filter((c) => !c.external); // Filter to classes under current scope const scopedClasses = []; for (const cls of allClasses) { if (scope.length > 0) { const parts = cls.fqcn.split(SEP); if (parts.length <= scope.length) continue; let match = true; for (let i = 0; i < scope.length; i++) { if (parts[i] !== scope[i]) { match = false; break; } } if (!match) continue; } scopedClasses.push(cls); } // Group by the first sub-namespace segment after the current scope. // Classes sitting directly at scope level get no container. const nsSet = new Set(); for (const cls of scopedClasses) { const parts = cls.fqcn.split(SEP); if (parts.length > scope.length + 1) { nsSet.add(parts.slice(0, scope.length + 1).join(SEP)); } } // Add namespace container nodes first (compound node parents must come before children) for (const nsPath of nsSet) { const parts = nsPath.split(SEP); const label = parts[scope.length]; // one-segment label, e.g. "Commands" nodes.push({ data: { id: 'ns-container::' + nsPath, label, nodeType: 'namespace-container', nsPath, }, }); } // Add class nodes as children of their namespace container for (const cls of scopedClasses) { const parts = cls.fqcn.split(SEP); const nsPath = parts.length > scope.length + 1 ? parts.slice(0, scope.length + 1).join(SEP) : null; const nodeData = { id: cls.fqcn, label: parts[parts.length - 1], fullLabel: cls.fqcn, type: cls.type, nodeType: 'class', external: false, namespace: cls.namespace, file: cls.file, line: cls.line, depCount: cls.dependencies ? cls.dependencies.length : 0, dependantCount: cls.dependants ? cls.dependants.length : 0, degree: degree.get(cls.fqcn) || 0, instability: cls.instability, fanOut: cls.fanOut, fanIn: cls.fanIn, }; if (nsPath) nodeData.parent = 'ns-container::' + nsPath; nodes.push({ data: nodeData }); } const renderedIds = new Set(scopedClasses.map((c) => c.fqcn)); const CONFIDENCE_RANK = { certain: 4, high: 3, medium: 2, low: 1 }; const edgeMap = new Map(); for (const e of data.edges) { if (!renderedIds.has(e.source) || !renderedIds.has(e.target)) continue; const key = e.source + '\u2192' + e.target; if (!edgeMap.has(key)) edgeMap.set(key, { source: e.source, target: e.target, entries: [] }); edgeMap.get(key).entries.push(e); } let ei = 0; for (const group of edgeMap.values()) { const { source, target, entries } = group; const best = entries.reduce((a, b) => (CONFIDENCE_RANK[b.confidence] || 0) > (CONFIDENCE_RANK[a.confidence] || 0) ? b : a ); edges.push({ data: { id: 'e' + ei++, source, target, weight: entries.length, confidence: best.confidence, edgeType: entries.map((e) => e.type).join(', '), entries, }, }); } return [...nodes, ...edges]; } export function navigateToScope(nsPath) { currentScope = nsPath ? nsPath.split('\\') : []; renderBreadcrumb(); emit('selection:cleared'); emit('namespace:rebuild'); } export function getCurrentScope() { return currentScope; } function renderBreadcrumb() { const el = document.getElementById('ns-breadcrumb'); if (!el) return; const parts = currentScope; const items = []; if (parts.length === 0) { items.push('<span class="breadcrumb-item breadcrumb-item--current">root</span>'); } else { items.push('<span class="breadcrumb-item" data-scope="">root</span>'); } for (let i = 0; i < parts.length; i++) { const sc = parts.slice(0, i + 1).join('\\'); items.push('<span class="breadcrumb-sep">?</span>'); if (i === parts.length - 1) { items.push( '<span class="breadcrumb-item breadcrumb-item--current">' + escHtml(parts[i]) + '</span>' ); } else { items.push( '<span class="breadcrumb-item" data-scope="' + escHtml(sc) + '">' + escHtml(parts[i]) + '</span>' ); } } const isClassMode = viewMode === 'classes'; const toggleLabel = isClassMode ? 'Namespaces' : 'Classes'; const toggleTitle = isClassMode ? 'Afficher par namespaces' : 'Afficher toutes les classes'; el.innerHTML = '<span class="breadcrumb-path">' + items.join('') + '</span>' + '<button class="view-mode-toggle' + (isClassMode ? ' view-mode-toggle--active' : '') + '" id="view-mode-toggle" title="' + toggleTitle + '">' + toggleLabel + '</button>'; el.querySelectorAll('.breadcrumb-item[data-scope]').forEach((item) => { item.addEventListener('click', () => navigateToScope(item.dataset.scope)); }); el.querySelector('#view-mode-toggle').addEventListener('click', () => { setViewMode(viewMode === 'folders' ? 'classes' : 'folders'); }); } function escHtml(str) { return str.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;'); } export function initNamespaceBrowser(data) { nsData = data; currentScope = []; renderBreadcrumb(); }