- Wprowadzenie
- Jak modyfikatory kształtują wzorce wyszukiwania
- Wykonywanie precyzyjnych zastąpień strukturalnych
- Brak dwuetapowego wyszukiwania w SSR
- Zaawansowane wzorce Script constraints - praktyczne zastosowania
- Znajdowanie wywołań loggera we wszystkich wzorcach dostępu
- Dopasowywanie zmiennych implementujących wiele interfejsów
- Dynamiczne generowanie listy metod z interfejsu
- Rozróżnianie metod obiektu vs callable properties
- Ograniczanie parametrów do typów string - kompletny przewodnik
- Regex z back references - kompletny przewodnik
- Zaawansowane heurystyki dla callable properties
- Wzorce dla nowoczesnego PHP 7/8
- Strategie wydajności Script constraints
- Integracja z przepływami pracy PhpStorm
- Ograniczenia Script constraints
- Indeks pojęć
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.
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ń.
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.
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.
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.
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.
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()
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.
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"
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.
$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
}
- SSR → znajdź wzorce strukturalne
- Find in Files (Regex) → znajdź definicje zmiennych:
\$nazwaZmiennej\s*=\s*["']([^"']*)["']
- Manual correlation → połącz wyniki ręcznie
// 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
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
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.
// 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.
$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())
}
$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
}
Problem: Jak zapewnić, że parametr to string literal lub zmienna typu string?
$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.
$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) // ❌
$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
}
$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("^['\"].*['\"]$")
}
// 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,}['"]$
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