După metodologia descrisă aici, am redenumit de mână câteva clase cu diverse grade ale interconectării cu restul modulului și am notat o serie de pași, particularități și dificultăți. Firește, prima și prima dată trebuie modificat fișierul principal ce conține clasa (ori interfața, ori alt artefact).

Bréviaire de Renaud de Bar, Bibliothèque d’Étude de Verdun
Trecând peste trivialul cât de greu poate să fie? s-ar putea descrie, în mare, următoarele etape:
– alegerea unui artefact de redenumit;
– definirea namespace-ului;
– mutarea verificării, folosind constanta ABP01_LOADED, că fișierul nu este apelat direct din browser;
– mutarea dependințelor declarate cu use în cadrul namespace-ului;
– redenumirea clasei;
– declararea dependintelor suplimentare necesare ca urmare a integrării într-un namespace;
– dacă nu este modificat comportamentul, activarea strict_typesși declararea tipului explicit pentru câmpuri și parametri (opțional, nu apare ca o consecință inevitabilă a restructurării);
– totul cu păstrarea antetului de licențiere în același loc.
Apoi trebuie căutat pe cine afectează modificarea: moșteniri, implementări, colaboratori și consumatori, importuri de abilități (trait), în apeluri la class_exists sau is_a, delegați specificați ori ca șir de caractere, ori folosind ::class, reflexivitate etc.
În sfârșit, restul operațiunilor trebuincioase:
– înregistrarea alias-ului pentru compatibilitate;
– dacă apare în lista de servicii injectabile, de reținut cel construit sub numele vechi și adăugat o înregistrare pentru numele nou;
– adăugarea meșei corespunzătoare în abp01-legacy-shim.php pentru satisfacere poftelor perverse ale analizorului static.
Este o munculiță teribil de angajantă și, cunoscându-mă prea bine, cu zero șanse de-a o termina de capul meu în următorii ani. Aș fi început să pigulesc scame sau ceva, prefer orice unui asemenea marasm. Așa că am pus la cale un mic șantan, un skill unde am trasat sarcinile de serviciu, pentru a-l putea folosi pe Manelică (zis și Stacky), agentul meu pentru susținere inginerească și emoțională.
Teoretic, se poate doar așa, primele experimente au fost îmbucurătoare. Practic, însă, există multe aspecte deterministe și n-are rost să folosești un agent, ar fi doar o splendidă pierdere de timp și lipsă de fantezie. Apoi, e nevoie de puțină disciplină, chiar cu riscul de-a împărți toată conversia pe mai multe versiuni.
Drept urmare, pe lângă regulile de bază, am definit două mici utilitare și o regulă că toate modificările se fac pe loturi restrânse, cu evaluarea rezultatelor după fiecare lot. Extinderea unui lot sau adăugarea altuia nou pentru o singură versiune trebuie argumentată.
Primul utilitar se ocupă exclusiv cu memoria, asigurând că agentul nu trebuie de fiecare dată să descopere întregul domeniu de clase de redenumit. Inventarul este salvat într-un fișier class-list.json și conține înregistrări precum:
"Abp01_AvailabilityHelper": {
"newClass": "WpTripSummary\\AvailabilityHelper",
"filePath": "lib/AvailabilityHelper.php",
"status": "pending",
"potentialIssues": []
}
Cheia este numele clasei vechi, restul cred că se înțeleg de la sine. Ce a fost procesat rămâne aici, cu "status": "complete". Acel "potentialIssues" se referă la atenționări pentru Manelică, cum ar fi:
– dacă tipul este o interfață, de avut grijă să fie migrată înaintea claselor ce-o implementează;
– la fel dacă este o clasă abstractă;
– dacă reprezintă o dependință injectabilă, atunci va trebui să fie gestionată așa cum am menționat.
Probabil o să definesc în versiunea următoara utilitare și pentru alte acțiuni complementare asupra inventarului, momentan însă îl las pe Manelică să-l actualizeze pe măsură ce procesează. Script-ul nu are nimic spectaculos ca realizare intelectuală, dar curioșii îl pot consulta aici. Se apelează simplu pe directorul dorit:
php .agents/skills/wpts-refactor-code-namespaces/scripts/build-class-list.php .
Cel de-al doilea utilitar a fost un pic mai angajant, rolul său fiind operarea modificărilor amintite anterior în fișierul migrat. Este un bun exemplu de utilizare a funcției native token_get_all(), ce segmentează un bloc de cod sursă dat în atomi lexicali. La prima vedere, un analizor modest; în realitate, suficient pentru sarcina de față și chiar versatil, a-propos, pentru alte chestiuni asemănătoare. Mai mult, e o muncă ce n-o faci și încă una sensibilă și considerabilă.
Fiedcare se poate spăla mai departe pe cap cu rezultatul după cum socotește de cuviință. Eu îl rețin într-un vector de vectori asociativi, ca identificator (tipul atomului: T_OPEN_TAG, T_WHITESPACE, T_CLASS, T_STRING etc.), textul aferent și poziția în fișier:
function tokenOffsets(string $source): array {
$offset = 0;
$result = array();
foreach (token_get_all($source) as $token) {
$text = is_array($token) ? $token[1] : $token;
$result[] = array(
'id' => is_array($token) ? $token[0] : null,
'text' => $text,
'offset' => $offset,
);
$offset += strlen($text);
}
return $result;
}
Armat cu cristalul acesta, am putut înainta pe teritoriul inamic. În primul rând, câteva verificări ce constituie și limitările de căpătâi:
– dacă detectez deja o declarație de namespace ies cu mesaj și cod explicite;
– idem dacă există mai mult de o clasă în acel fișier…
– ori de nu există un indicator <?php.
Primele două țin strict de o bună orânduială. În general am fost suficient de riguros cât să nu-ncalc regulile, dar, dacă se intâmplă, ar fi o problemă ce-ar trebui gestionată deliberat și, cel mai probabil, de la caz la caz. După cum spune și marele filozof și sociolog Remo Gaggi:
Frankie, the little guy. He wouldn’t be fucking the Jew’s wife, would he? Because if he is, it’s a problem.
Apoi, dintre toate etapele de procesare, poate cea mai fercheșă este detectarea dependințelor, pentru a ști ce instrucțiuni use să generez, devenite obligatorii odată cu mutarea clasei din contextul global în cel determinat de un namespace, dacă vreau să păstrez referințele necalificate din cod. Chestiunea necesită mai multă atenție decât ar reclama aparențele. Astfel, avem dependințe introduse de:c
– instanțiere, verificarea folosind instanceof ori de moștenire – acestea vin doar câte una per fiecare din operatorii: T_NEW, T_INSTANCEOF, T_EXTENDS;
– implementarea uneia sau mai multor interfețe, deci operatorul T_IMPLEMENTS presupune mai multe potențiale dependințe;
– gestionarea excepțiilor;
– apeluri statice, deci cu :: (T_DOUBLE_COLON);
– tipurile specificate pentru valorile de ieșire ale funcțiilor (cu atenție foarte mare la identificatori ce reprezintă scalari ori la identificatori speciali precum void);
– tipurile declarate pentru parametrii funcțiilor (idem);
– trait-uri incluse cu use.
function determineClassDependencies(array $tokens, string $legacyName): array {
$dependencies = array();
$previousMeaningfulToken = null;
$expectSingleClass = false;
$inImplementsList = false;
$inTraitUseList = false;
$braceDepth = 0;
$inCatchType = false;
$waitingForCatchParenthesis = false;
$inFunctionDefinition = false;
$hasPassedArgumentsDeclaration = false;
$classNamesAfter = array(T_NEW,
T_INSTANCEOF,
T_EXTENDS);
$excludeTypeHints = array(
'mixed',
'array',
'string',
'int',
'float',
'bool',
'self',
'void',
'never',
'true',
'false',
'null'
);
foreach ($tokens as $index => $token) {
if (isIgnoredToken($token)) {
continue;
}
$braceDepth = getBraceDepthAfterToken($token,
$braceDepth);
if ($token['id'] === T_USE && $braceDepth > 0) {
// Namespace imports are top-level in the
// accepted source files.
// Closure captures start with '(', whereas trait lists
// start with a name.
$nameIndex = nextMeaningfulTokenIndex($tokens, $index + 1);
if ($nameIndex !== null && isClassNameToken($tokens[$nameIndex])) {
$inTraitUseList = true;
$previousMeaningfulToken = $token;
continue;
}
}
if (in_array($token['id'],
$classNamesAfter,
true)) {
$expectSingleClass = true;
$previousMeaningfulToken = $token;
continue;
}
if ($token['id'] === T_IMPLEMENTS) {
$inImplementsList = true;
$previousMeaningfulToken = $token;
continue;
}
if ($token['id'] === T_CATCH) {
$waitingForCatchParenthesis = true;
$previousMeaningfulToken = $token;
continue;
}
if ($waitingForCatchParenthesis && $token['text'] === '(') {
$waitingForCatchParenthesis = false;
$inCatchType = true;
$previousMeaningfulToken = $token;
continue;
}
if ($token['id'] === T_FUNCTION) {
$inFunctionDefinition = true;
continue;
}
if ($inFunctionDefinition === true && $token['text'] === '(') {
//Not doing this will add the function name as a dependency,
// since the token type is reported as T_STRING!!!
$hasPassedArgumentsDeclaration = true;
continue;
}
if ($inFunctionDefinition === true
&& in_array($token['text'],
array('{', ';'),
true)) {
$inFunctionDefinition = false;
$hasPassedArgumentsDeclaration = false;
continue;
}
if (isClassNameToken($token)) {
if ($expectSingleClass
|| $inImplementsList
|| $inCatchType
|| $inTraitUseList) {
addClassDependency($dependencies,
$token['text'],
$legacyName);
$expectSingleClass = false;
} elseif ($inFunctionDefinition
&& $hasPassedArgumentsDeclaration
&& !in_array($token['text'],
$excludeTypeHints,
true)) {
addClassDependency($dependencies,
$token['text'],
$legacyName);
} elseif ($previousMeaningfulToken !== null
&& $previousMeaningfulToken['id'] === T_DOUBLE_COLON) {
// The class name is before ::, so it is handled when :: is reached.
}
}
if ($token['id'] === T_DOUBLE_COLON
&& $previousMeaningfulToken !== null
&& isClassNameToken($previousMeaningfulToken)) {
addClassDependency($dependencies,
$previousMeaningfulToken['text'],
$legacyName);
}
if (($inImplementsList || $inTraitUseList)
&& in_array($token['text'],
array('{', ';'),
true)) {
$inImplementsList = false;
$inTraitUseList = false;
}
if ($inCatchType
&& ($token['id'] === T_VARIABLE
|| $token['text'] === ')')) {
$inCatchType = false;
}
if ($expectSingleClass && ($token['id'] === T_VARIABLE
|| $token['id'] === T_CLASS)) {
// Dynamic instantiation or an anonymous class
// cannot be imported.
$expectSingleClass = false;
}
$previousMeaningfulToken = $token;
}
return array_keys($dependencies);
}
Înlocuirea propriu-zisă a referințelor am implementat-o-n doi pași: mai întâi prepar la foc mic și sare după gust un vector de valori destinate substr_replace (adică poziția originală, lungimea simbolului și noua valoare, adică noul nume), apoi apelez funcția pentru fiecare după ce sortez vectorul după poziția originală:
function buildReplacements(array $tokens,
string $legacyName,
string $shortName): array {
//Find references to legacy name and register a replacement
// for each (tailored for substr_replace).
// A replacement item holds:
// - the original offset,
// - original symbol length
// - and new value, i.e. the new short name
$replacements = array();
foreach ($tokens as $token) {
if ($token['id'] === T_STRING && $token['text'] === $legacyName) {
$replacements[] = array(
'offset' => $token['offset'],
'length' => strlen($legacyName),
'value' => $shortName,
);
}
}
return $replacements;
}
function applyReplacements(string $source, array $replacements): string {
// Spaceship operator:
// - returns 0 if values on either side are equal
// - returns 1 if the value on the left is greater
// - returns -1 if the value on the right is greater
usort($replacements,
static fn(array $left, array $right): int
=> $right['offset'] <=> $left['offset']
);
foreach ($replacements as $replacement) {
$source = substr_replace(
$source,
$replacement['value'],
$replacement['offset'],
$replacement['length']
);
}
return $source;
}
Restul, deși nu chiar plictisitor, e oarecum oblu până la capăt și poate fi consultat pe GitHub. Ar fi nevoie de un utilitar similar pentru a înlocui mai departe referințele către clasa proaspăt redenumită, procedura fiind, evident, mai simplă. Dar… versiunea următoare. Momentan Manelică s-a achitat foarte bine de sarcină.
Apelarea este la fel de simplă și se va observa un ultim mecanism de precauție, anume rularea fără scrierea rezultatelor, pentru verificare finală, foarte util și pentru mine în evaluarea rezultatelor (--dry-run):
php .agents/skills/.../change-target-class-identifier.php <file> <legacy-name> [--dry-run]
Înainte să trag ștecherul, un cuvânt despre loturi. Sunt tot fișiere json, într-un director al lor, bazate pe class-list.json. Procedura este că negociez cu Manelică ce ar trebui făcut – a se citi are instrucțiuni să-mi tempereze avântul revoluționar, se notează observații și abia apoi se procedează la modificarea propriu-zisă.
Motivul frânei este nu doar disciplina, nici doar controlul, cât și faptul că nu tot ce se află acum în plug-in va rămâne și atunci va fi un efort irosit sau prost-direcționat. Bunăoară, procesarea fișierelor KML este acum un pachet separat. Este și o fază exploratorie, unele dezavantaje poate nu sunt întotdeauna imediat vizibile, iar un agent este mult mai eficient decât mine în căutarea și indexarea lor.
Instrucțiunile formale sunt în fișierul markdown SKILL.md, într-o formă mai elaborată decât ce am povestit eu aici. Iar dacă pare o bătaie prea mare de cap… după cum ziceam, orice mai puțin să le redenumesc cu mâna. Perfidiile procrastinării inginerești sunt inepuizabile.