Upiększanie kodu w PHP za pomocą method chaining
PHP jest uważany za brzydki język. Ewoluował on kawałek po kawałku i to widać. Ma jednak wiele zalet. Zespół programistów nieustannie go ulepsza i z każdą wersją, w szczególności PHP 7, czyni on wielkie postępy, zarówno pod względem funkcji, jak i wydajności.
Inne zalety tego języka, to przystępna dokumentacja, wszechobecny hosting i znaczna ilość zasobów online. I tak, wszystkie największe platformy CMS (Wordpress, Drupal, Joomla) są oparte na języku PHP.
Jak można zatem napisać piękny kod w PHP?
Krytykanci powiedzą, że to niemożliwe, ale frameworki takie jak Laravel („framework PHP dla rzemieślników internetowych”) już udowodniły, że się mylą. Jednym z wzorców, które Laravel i inne szanowane frameworki PHP często implementują, jest method-chaining, czyli łańcuchowe wywołania.
Oto przykład tego, o czym mówię: API do tworzenia zapytań Laravela:
$users = DB::table('users')
->where('votes', '>', 100)
->orWhere('name', 'John')
->get();
Ta „łańcuchowa” składnia jest niezła, ponieważ pozwala programistom pisać kod, który wykonuje komendę w ekspresyjny i czytelny sposób. W powyższym przykładzie można zobaczyć, co się dzieje:
- Spójrz na wiersze w tabeli
users
. - Gdzie wartość votes jest wyższa niż 100.
- ...lub gdzie nazwa user to John.
- Pobierz wyniki.
Całkiem zgrabnie, prawda? Zobaczmy, jak wprowadzić ten schemat w życie.
Praktyka
Aby zilustrować method-chaining, spójrzmy na operacje tablicowe. PHP ma wiele pomocnych funkcji tablicowych, takich jak array_map
, array_filter
, usort
itp., ale wykonywanie wielu operacji na jednej tablicy może zaboleć.
Załóżmy, że mamy tablicę nazw postaci z Simpsonów i chcemy zastosować następujące transformacje:
- Wyeliminuj wszystkie nazwy, które nie kończą się na „Simpson”.
- Zamień imię i nazwisko na imię, np.
Lisa Simpson
naLisa
. - Sortuj nasze wyniki alfabetycznie.
Oto jeden ze sposobów rozwiązania naszego problemu:
$characters = [
'Maggie Simpson',
'Edna Krabappel',
'Marge Simpson',
'Lisa Simpson',
'Moe Szyslak',
'Waylon Smithers',
'Homer Simpson',
'Bart Simpson'
];
// Look for full names that end in Simpson
// Filter out items that don't match
$simpsons = array_filter($characters, function ($character) {
return preg_match('/^.+\sSimpson$/', $character);
});
// Replace " Simpson" with an empty string for each item
$simpsons = array_map(function ($character) {
return str_replace(' Simpson', '', $character);
}, $simpsons);
// Sort the items using PHP's "strcasecmp"
usort($simpsons, function ($a, $b) {
return strcasecmp($a, $b);
});
var_dump($simpsons); // ['Bart', 'Homer', 'Lisa', 'Maggie', 'Marge']
To daje nam poprawny wynik, ale bystrzaki mogą zwrócić uwagę na kilka drobiazgów:
array_filter
akceptuje zarówno tablicę, jak i callback.array_map
też, ale w odwrotnej kolejności. Dlaczego?- Operacje filtrowania i mapowania są zapisywane w zmiennej
$simpsons
, aleusort
uzyskuje dostęp do naszej tablicy przez referencję. Skąd ta niespójność? - To nie wygląda zbyt ładnie.
Zobaczmy dla porównania, jak można rozwiązać ten problem w JavaScript:
const simpsons = characters
.filter(character => character.match(/^.+\sSimpson$/))
.map(character => character.replace(' Simpson', ''))
.sort((a, b) => b < a)
console.log(simpsons)
W JavaScript wygląda to o wiele bardziej elegancko. Tablice są rodzajem obiektu, co pozwala na połączenie operacji tablicowych. Ponadto, w powyższym przykładzie JS używa funkcji strzałkowych, żeby zachować zwięzłość. PHP jeszcze tego nie potrafi, ale już niedługo może umieć!
Method chaining w tablicach przychodzi łatwo, jeśli piszemy w JavaScript. Możemy to jednak naśladować w PHP, pisząc własną klasę.
Tworzenie klasy obsługującej method-chaining
Nazwijmy naszą klasę Collection
. Instancję tej klasy można utworzyć, przekazując tablicę do konstruktora.
class Collection
{
private $array;
public function __construct($array)
{
$this->array = $array;
}
}
$characters = new Collection([
'Maggie Simpson',
'Edna Krabappel',
'Marge Simpson',
'Lisa Simpson',
'Moe Szyslak',
'Waylon Smithers',
'Homer Simpson',
'Bart Simpson'
]);
Do tej pory nasza klasa niewiele robi - po prostu zapisuje tablicę przekazaną jako właściwość prywatną. Skoncentrujmy się na dodaniu publicznej metody filtrowania.
public function filter($callback)
{
$this->array = array_filter($this->array, $callback);
return $this;
}
Zamiast przekazywać zarówno tablicę, jak i callback (jak poprzednio), teraz możemy przekazać jedynie funkcję callback. Gdy wykonamy przekształcenie na własności instancji, zostanie zwrócona sama instancja. Dzięki temu możliwy jest method-chaining.
Dodajmy teraz metody dla map
i sort
:
public function map($callback)
{
$this->array = array_map($callback, $this->array);
return $this;
}
public function sort($callback)
{
usort($this->array, $callback);
return $this;
}
Teraz nasze metody są gotowe do wywołania łańcuchowego, ale nadal potrzebujemy pomysłu na uzyskanie końcowego rezultatu jako tablicy. Można to zrobić w sposób podobny do tego, w jaki Laravel używa get()
do wykonania zapytania po zbudowaniu go z różnych warunków.
public function execute()
{
return $this->array;
}
Podsumowując, nasza klasa wygląda następująco:
class Collection
{
private $array;
public function __construct($array)
{
$this->array = $array;
}
public function filter($callback)
{
$this->array = array_filter($this->array, $callback);
return $this;
}
public function map($callback)
{
$this->array = array_map($callback, $this->array);
return $this;
}
public function sort($callback)
{
usort($this->array, $callback);
return $this;
}
public function execute()
{
return $this->array;
}
}
Wreszcie możemy wywołać łańcuch metod na tablicy:
$characters = new Collection([
'Maggie Simpson',
'Edna Krabappel',
'Marge Simpson',
'Lisa Simpson',
'Moe Szyslak',
'Waylon Smithers',
'Homer Simpson',
'Bart Simpson'
]);
$simpsons = $characters
->filter(function ($character) {
return preg_match('/^.+\sSimpson$/', $character);
})
->map(function ($character) {
return str_replace(' Simpson', '', $character);
})
->sort(function ($a, $b) {
return strcasecmp($a, $b);
})
->execute();
var_dump($simpsons); // ['Bart', 'Homer', 'Lisa', 'Maggie', 'Marge']
Bardzo w stylu JavaScript! Problemy z kolejnością argumentów i dostępem do referencji nadal istnieją, ale nasza klasa Collection
radzi sobie z tym za kulisami. Rezultatem jest kod, który jest znacznie czystszy i bardziej czytelny!
Podsumowanie
Język PHP ma złą reputację z powodu bałaganu, jaki w nim panuje, ale Laravel i inne frameworki używają już metod łańcuchowych (między innymi), które upiększają kod.
W przeciwieństwie do JavaScript, operacje tablicowe w PHP nie są łańcuchowe, ale można użyć podstawowej klasy pomocniczej (takiej, jak nasza klasa Collection
!), aby to naśladować i utrzymać niektóre dziwactwa PHP z dala od naszych oczu.