Skip to content

Instantly share code, notes, and snippets.

@gander
Last active August 25, 2025 13:38
Show Gist options
  • Save gander/8fe726c61ffa8545039a5d115d13caf2 to your computer and use it in GitHub Desktop.
Save gander/8fe726c61ffa8545039a5d115d13caf2 to your computer and use it in GitHub Desktop.
PhpStorm Structural Search and Replace - Kompletny przewodnik

PhpStorm Structural Search and Replace - Kompletny przewodnik

Spis treści

  1. Wprowadzenie
  2. Jak modyfikatory kształtują wzorce wyszukiwania
  3. Wykonywanie precyzyjnych zastąpień strukturalnych
  4. Brak dwuetapowego wyszukiwania w SSR
  5. Zaawansowane wzorce Script constraints - praktyczne zastosowania
  6. Wzorce dla nowoczesnego PHP 7/8
  7. Strategie wydajności Script constraints
  8. Integracja z przepływami pracy PhpStorm
  9. Ograniczenia Script constraints
  10. Indeks pojęć

Wprowadzenie

PhpStorm Structural Search and Replace (SSR) to potężne narzędzie umożliwiające wyszukiwanie i modyfikację kodu PHP na podstawie struktury składniowej, a nie dosłownego tekstu. Wykorzystuje zmienne szablonowe jak $zmienna$ w połączeniu z modyfikatorami, tworząc zaawansowane wzorce wyszukiwania rozumiejące semantykę języka PHP, co czyni je nieocenionym przy refaktoryzacji na dużą skalę, kontroli jakości kodu i wykrywaniu złożonych wzorców.

SSR fundamentalnie zmienia podejście do wyszukiwania i modyfikacji kodu, traktując go jako strukturalne drzewo, a nie płaski tekst. Dostęp do narzędzia: Edit → Find → Search Structurally (Ctrl+Shift+S) do wyszukiwania lub Replace Structurally (Ctrl+Shift+R) do zastępowania. Siła systemu bierze się z możliwości zastosowania wielu ograniczeń do zmiennych szablonowych, tworząc precyzyjne wzorce dopasowujące dokładnie to, czego potrzebujesz, ignorując nieistotne różnice w formatowaniu, białych znakach czy strukturze kodu.

Jak modyfikatory kształtują wzorce wyszukiwania

Modyfikator Count kontroluje częstotliwość wystąpień

Modyfikator Count określa, ile razy zmienna może wystąpić w dopasowanym kodzie. Ustawienie min=1, max=3 na $metoda$ w szablonie klasy znajdzie klasy z 1-3 metodami, podczas gdy pozostawienie pustego max pozwala na nieograniczoną liczbę wystąpień. Ten modyfikator okazuje się niezbędny przy wyszukiwaniu określonych wzorców architektonicznych - na przykład znajdowaniu klas singleton z dokładnie jednym prywatnym konstruktorem i jedną publiczną metodą statyczną.

// Znajdź klasy z 1-3 publicznymi metodami
class $Klasa$ {
    public function $metoda$() {}
}
// Ustaw $metoda$ Count: min=1, max=3

Częsty błąd to zapomnienie, że puste pole max oznacza nieograniczoną liczbę, a nie zero. Przy wyszukiwaniu opcjonalnych elementów jawnie ustaw min=0 i max=1, aby uniknąć nieoczekiwanych dopasowań.

Modyfikator Type wymusza typy danych PHP

Ograniczenia Type ograniczają zmienne do określonych typów PHP lub implementacji klas. Wbudowane typy to string, int, float, boolean, array i null. Dla typów klasowych używaj pełnych nazw z wiodącym ukośnikiem: \DateTime lub \App\Models\User.

// Znajdź przypisania zmiennych typu string
$var$ = $wartosc$;
// Ustaw $wartosc$ Type: string

// Znajdź parametry metod typu DateTime
public function $metoda$($param$) {}
// Ustaw $param$ Type: \DateTime

Modyfikator staje się szczególnie potężny przy wyszukiwaniu implementacji interfejsów lub określonych wzorców dziedziczenia, choć wymaga dokładnego dopasowania typu, a nie akceptuje domyślnie typów pochodnych.

Modyfikator Text stosuje wzorce regex

Modyfikatory Text umożliwiają dopasowywanie wyrażeń regularnych do nazw zmiennych lub zawartości. Modyfikator obsługuje pełną składnię regex, włączając flagi inline jak (?i) dla dopasowania bez uwzględniania wielkości liter.

// Znajdź metody magiczne
$obiekt$->$metoda$()
// Ustaw $metoda$ Text: ^__.+$

// Znajdź gettery (bez uwzględniania wielkości liter)
public function $nazwaMetody$() {}
// Ustaw $nazwaMetody$ Text: (?i)^get.*

Wydajność spada przy zbyt złożonych wzorcach regex, więc preferuj proste wyrażenia gdy to możliwe. Modyfikator doskonale sprawdza się przy egzekwowaniu konwencji nazewnictwa - znajdowaniu metod naruszających zasady camelCase lub stałych nie w formacie UPPER_CASE.

Modyfikator Script umożliwia złożoną logikę

Ograniczenia Script używają kodu Groovy do dostępu do drzewa PSI (Program Structure Interface), zapewniając bezprecedensową elastyczność dopasowywania. Zmienne stają się węzłami PSI z dostępnymi właściwościami jak .text, .name i .parent.

// Znajdź wywołania metod z długimi parametrami string
$obiekt$->$metoda$($param$)
// Ustaw $param$ Script: param.text.length() > 50

// Znajdź niepasujące nazwy getterów
public function $metoda$() {
    return $this->$pole$;
}
// Script: !metoda.name.substring(3).toLowerCase().equals(pole.name)

Modyfikator Script oferuje metody jak zmienna.getContainingClass(), zmienna.getParameterList() i operacje na stringach włączając .contains(), .startsWith() i .toLowerCase(). Skrypty mogą odwoływać się do innych zmiennych w tym samym szablonie, umożliwiając logikę walidacji między-zmiennymi.

Modyfikator Reference ponownie używa zapisanych szablonów

Modyfikatory Reference pozwalają na osadzanie jednego szablonu SSR w drugim, tworząc hierarchiczne wzorce wyszukiwania. Zapisz często używane wzorce jako szablony, następnie odwołuj się do nich po nazwie w polu Reference.

// Zapisz szablon "metody_statyczne": public static function $metoda$() {}
// Następnie odwołaj się w innym wyszukiwaniu:
$Klasa$::$wywolanie$()
// Ustaw $wywolanie$ Reference: metody_statyczne

To podejście zapobiega duplikacji wzorców i umożliwia budowanie złożonych wyszukiwań z prostszych komponentów. Upewnij się, że szablony, do których się odwołujesz, istnieją przed użyciem, aby uniknąć błędów wyszukiwania.

Wykonywanie precyzyjnych zastąpień strukturalnych

Mechanika zmiennych szablonowych w zastąpieniach

Szablony zastąpień automatycznie uzyskują dostęp do wszystkich zmiennych przechwyconych podczas wyszukiwania. Zmienne zachowują swoje nazwy między wzorcami wyszukiwania i zastąpienia, umożliwiając płynne przekazywanie wartości. Składnia pozostaje spójna: $zmienna$ w wyszukiwaniu staje się $zmienna$ w zastąpieniu.

// Szukaj: $obj$->staraMetoda($args$)
// Zastąp: $obj$->nowaMetoda($args$)

Zmienne mogą podlegać transformacji podczas zastępowania używając modyfikatorów Script. Uzyskaj dostęp do przechwyconego tekstu z zmienna.getText() i manipuluj nim używając operacji stringowych Groovy:

// Konwertuj dostęp do właściwości na metodę getter
// Szukaj: $obj$->$wlasciwosc$
// Zastąp: $obj$->get$Wlasciwosc$()
// $Wlasciwosc$ Script: wlasciwosc.getText().capitalize()

Zaawansowane techniki zastępowania

Zastąpienia warunkowe wykonują się tylko gdy spełnione są określone kryteria. Dodaj ograniczenia Script, aby kontrolować kiedy zastąpienia następują:

// Zastąp tylko w kontekstach deprecated
// Script: zmienna.parent.text.contains('deprecated')

Wielokrotne podstawienia zmiennych pozwalają na złożone transformacje, gdzie różne części zmieniają się niezależnie. Każda zmienna może mieć własną logikę transformacji:

// Szukaj: $prefiks$_$sufiks$
// Zastąp: nowy$Prefiks$_$sufiks$
// $Prefiks$ Script: prefiks.getText().capitalize()

PhpStorm domyślnie zachowuje formatowanie i komentarze w dopasowanych blokach. Opcja "Reformat" automatycznie stosuje reguły stylu kodu do zastąpionego kodu. Dla zastąpień wrażliwych na białe znaki użyj celu "Complete match", aby uwzględnić otaczający kontekst.

Wzorce zastąpień specyficzne dla PHP

Refaktoryzacja sygnatur metod korzysta ze zrozumienia składni PHP przez SSR:

// Konwertuj wywołania statyczne na instancyjne
// Szukaj: MojaKlasa::$metoda$($args$)
// Zastąp: $this->$metoda$($args$)

// Zamień kolejność parametrów
// Szukaj: $obj$->process($param1$, $param2$)
// Zastąp: $obj$->process($param2$, $param1$)

Modernizacja składni tablic demonstruje możliwości transformacji masowej:

// Szukaj: array($elementy$)
// Zastąp: [$elementy$]

Modyfikacje namespace wykorzystują świadomość namespace PhpStorm:

// Szukaj: new \Full\Namespace\$Klasa$($args$)
// Zastąp: new $Klasa$($args$)
// Włącz opcję "Shorten fully-qualified names"

Brak dwuetapowego wyszukiwania w SSR

Dlaczego SSR nie może łączyć wyszukiwań

SSR analizuje tylko jeden wzorzec strukturalny na raz i nie ma mechanizmu:

  • Łączenia wyników z różnych wzorców
  • Cross-reference między wyszukiwaniami
  • Przechowywania stanu między szukaniami
  • Korelacji danych z różnych miejsc w kodzie

Przykład problemu: Jeśli chcesz znaleźć wywołanie $obj->method($var) gdzie $var jest zdefiniowana gdzie indziej jako $var = "tekst", SSR nie potrafi automatycznie połączyć tych dwóch miejsc w kodzie.

Obejścia dla złożonej analizy

Metoda 1: Script z przeszukiwaniem lokalnym

$var$->$method$($param$)

// Script dla $param$ - szuka definicji w tym samym pliku:
try {
    if (param.getText().startsWith('$')) {
        def varName = param.getText()
        def fileText = param.getContainingFile().getText()
        
        // Regex do znajdowania przypisania w tym samym pliku
        def pattern = "\\${varName.substring(1)}\\s*=\\s*['\"]([^'\"]*)['\"]"
        def matcher = fileText =~ pattern
        
        if (matcher) {
            // Znaleziono definicję w tym samym pliku
            return true
        }
    }
    return true // Pokaż wszystkie wywołania
} catch (Exception e) {
    return true
}

Metoda 2: Kombinacja SSR + Find in Files

  1. SSR → znajdź wzorce strukturalne
  2. Find in Files (Regex) → znajdź definicje zmiennych:
    \$nazwaZmiennej\s*=\s*["']([^"']*)["']
  3. Manual correlation → połącz wyniki ręcznie

Metoda 3: Ograniczenie do lokalnego scope

// W obrębie jednej metody
public function $methodName$() {
    $var$ = $value$;
    $obj$->method($var$);
}

// Script dla $var$ (drugie wystąpienie):
// Sprawdź czy nazwa zmiennej pasuje do definicji w tej samej metodzie

Kiedy używać SSR vs inne narzędzia

Używaj SSR gdy:

  • Szukasz określonej struktury kodu w jednym miejscu
  • Chcesz refaktoryzować według wzorca
  • Analizujesz w obrębie jednego kontekstu lokalnego

Używaj innych narzędzi gdy:

  • Potrzebujesz cross-file analysis → Find Usages (Alt+F7)
  • Śledzisz data flow → Call Hierarchy (Ctrl+Alt+H)
  • Analizujesz powiązania między kodem → Static analysis tools

Znajdowanie wywołań loggera we wszystkich wzorcach dostępu

Problem: Jak jednocześnie znaleźć $logger->info('nope') i $this->logger->info('nope')?

Rozwiązanie 1: Script constraint (najbardziej elastyczne)

$accessor$->$metoda$($argumenty$)

// Script dla $accessor$ - dopasowuje różne wzorce dostępu:
try {
    def text = accessor.getText()
    
    // Sprawdź konkretne wzorce
    if (text.equals('$logger') || text.equals('$this->logger')) {
        return true
    }
    
    // Sprawdź ogólne wzorce logger access
    return text.contains("logger") || 
           text.matches(".*\\$[a-zA-Z_][a-zA-Z0-9_]*->logger") ||
           text.matches("self::\\$logger") ||
           text.matches("static::\\$logger")
           
} catch (Exception e) {
    return false
}

Rozwiązanie 2: Text modifier z regex

$accessor$->info($message$)

// Text modifier dla $accessor$:
^\$(?:this->)?logger$

// To znajdzie:
// $logger->info('nope')      ✅
// $this->logger->info('nope') ✅
// $service->logger->info()   ❌ (nie pasuje do wzorca)

Rozwiązanie 3: Wzorzec z opcjonalną częścią

$obj$->$logger$->info($message$)

// Count modifier dla $obj$: min=0, max=1 (opcjonalny)
// Text modifier dla $logger$: ^logger$

// Znajduje:
// $logger->info() gdy $obj$ jest puste
// $this->logger->info() gdy $obj$ to $this

Ten uniwersalny wzorzec skutecznie przechwytuje wszystkie wzorce użycia loggera PSR-3 w kodzie, czyniąc go nieocenionym przy auditach logowania lub migracjach między bibliotekami logowania.

Dopasowywanie zmiennych implementujących wiele interfejsów

// Wzorzec dla przypisań zmiennych
$var$ = new $klasa$($args$);

// Script constraint dla $klasa$:
import com.intellij.psi.*
def psiClass = klasa.resolve()
if (psiClass instanceof PsiClass) {
    def interfaces = psiClass.getImplementsList()?.getReferenceElements()
    return interfaces?.any { 
        it.getReferenceName() in ['InterfejsA', 'InterfejsB'] 
    }
}
return false

Wzorzec identyfikuje punkty kodu polimorficznego, gdzie mogą być używane różne implementacje, niezbędny przy analizie dependency injection lub refaktoryzacji segregacji interfejsów.

Dynamiczne generowanie listy metod z interfejsu

$va$->$met$($arg$)

// Script dla $met$ - dynamicznie sprawdza czy metoda jest z interfejsu:
try {
    import com.jetbrains.php.PhpIndex
    
    def methodName = met.getText()
    def project = met.getProject()
    def phpIndex = PhpIndex.getInstance(project)
    
    // Znajdź interfejs po nazwie
    def interfaces = phpIndex.getInterfacesByName("TwojInterfejs")
    
    if (!interfaces.isEmpty()) {
        def targetInterface = interfaces.first()
        def interfaceMethods = targetInterface.getMethods()
        
        // Sprawdź czy nazwa metody jest w interfejsie
        def methodNames = interfaceMethods.collect { it.getName() }
        return methodNames.contains(methodName)
    }
    
    return false  // Nie znaleziono interfejsu
    
} catch (Exception e) {
    // Fallback na statyczną listę
    def knownMethods = ['getName', 'setName', 'process', 'handle', 'execute']
    return knownMethods.contains(met.getText())
}

Rozróżnianie metod obiektu vs callable properties

$var$->$method$($arg$)

// Script dla $method$ (filtruje tylko prawdziwe metody):
try {
    def varElement = var
    def methodName = method.getText()
    
    // Sprawdź czy to rzeczywiście metoda w klasie
    def varType = varElement.getType()
    if (varType != null) {
        def psiClass = varType.resolve()
        if (psiClass instanceof com.intellij.psi.PsiClass) {
            def methods = psiClass.getMethods()
            def hasMethod = methods.any { it.getName() == methodName }
            return hasMethod  // true tylko jeśli to prawdziwa metoda
        }
    }
    
    // Fallback - sprawdź czy wygląda jak metoda (nie property)
    return methodName.matches("[a-zA-Z_][a-zA-Z0-9_]*") && 
           !methodName.contains('$')
           
} catch (Exception e) {
    return true  // W razie błędu, pokaż wszystko
}

Ograniczanie parametrów do typów string - kompletny przewodnik

Problem: Jak zapewnić, że parametr to string literal lub zmienna typu string?

Metoda 1: Type modifier (podstawowe typy PHP)

$obj$->$method$($param$)

// Type modifier dla $param$:
string

// To znajdzie:
$obj->method("tekst")     // ✅
$obj->method('tekst')     // ✅  
$obj->method($stringVar)  // ✅ jeśli PhpStorm wie że to string
$obj->method(123)         // ❌
$obj->method($intVar)     // ❌

Ograniczenie: Type modifier działa tylko gdy PhpStorm może określić typ statycznie.

Metoda 2: Text modifier z regex (dla string literalów)

$obj$->$method$($param$)

// Text modifier dla $param$ - tylko string literals:
^["'].*["']$

// Lub bardziej precyzyjny regex:
^(['"][^'"]*['"]|'[^']*'|"[^"]*")$

To znajdzie:

$obj->method("text")      // ✅
$obj->method('text')      // ✅
$obj->method("it's ok")   // ✅
$obj->method('say "hi"')  // ✅
$obj->method($variable)   // ❌
$obj->method(123)         // ❌

Metoda 3: Script constraint - tylko string literały

$obj$->$method$($param$)

// Script dla $param$ - sprawdza czy to string literal:
try {
    def paramText = param.getText()
    
    // Sprawdź czy zaczyna i kończy się cudzysłowami
    def isSingleQuoted = paramText.startsWith("'") && paramText.endsWith("'")
    def isDoubleQuoted = paramText.startsWith('"') && paramText.endsWith('"')
    
    return isSingleQuoted || isDoubleQuoted
    
} catch (Exception e) {
    return false
}

Metoda 4: Script - string literal LUB zmienna typu string

$obj$->$method$($param$)

// Script dla $param$ - string literal LUB zmienna string:
try {
    def paramText = param.getText()
    
    // Sprawdź czy to string literal
    if (paramText.matches("^['\"][^'\"]*['\"]$")) {
        return true
    }
    
    // Sprawdź czy to zmienna typu string
    if (paramText.startsWith('$')) {
        def paramType = param.getType()
        if (paramType != null) {
            return paramType.toString().contains("string")
        }
    }
    
    return false
    
} catch (Exception e) {
    // Fallback - tylko string literals
    return paramText.matches("^['\"].*['\"]$")
}

Regex patterns dla różnych formatów stringów

// Podstawowy string literal (pojedyncze lub podwójne cudzysłowy)
^['"][^'"]*['"]$

// String z escapowanymi znakami
^(['"][^'"\\]*(?:\\.[^'"\\]*)*['"])$

// Tylko pojedyncze cudzysłowy
^'[^']*'$

// Tylko podwójne cudzysłowy  
^"[^"]*"$

// String zawierający określony tekst
^['"].*error.*['"]$

// String niepusty
^['"].+['"]$

// String o określonej długości (min 3 znaki bez cudzysłowów)
^['"].{3,}['"]$

Praktyczne zastosowania

Znajdź hardcoded URLs:

$client$->$method$($url$)

// Script dla $url$:
try {
    def urlText = url.getText()
    
    // Musi być string literal
    if (!urlText.matches("^['\"][^'\"]*['\"]$")) {
        return false
    }
    
    // Sprawdź czy wygląda jak URL
    def content = urlText.toLowerCase()
    return content.contains("http://") || 
           content.c
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment