Klasa Promise w PHP i jej zastosowanie
Programowanie asynchroniczne zyskuje coraz większe znaczenie w nowoczesnym tworzeniu aplikacji, szczególnie tych pracujących w środowiskach serwerowych i obsługujących dużą liczbę żądań jednocześnie. W świecie PHP, który tradycyjnie był skoncentrowany na synchronicznym wykonywaniu kodu, asynchroniczność zaczęła odgrywać istotną rolę wraz z pojawieniem się nowych bibliotek i rozszerzeń. Jednym z kluczowych narzędzi w tym zakresie jest klasa Promise.
Co to jest Promise?
Klasa Promise to abstrakcja pozwalająca zarządzać operacjami asynchronicznymi w prosty i efektywny sposób. Podstawowym celem Promise jest przekazanie wyniku operacji asynchronicznej (sukcesu lub błędu) w przyszłości, gdy operacja ta zostanie zakończona.
Promise można wyobrazić sobie jako kontrakt, który może zostać spełniony (“zrealizowany”) lub odrzucony (“zawiedziony”). W PHP Promise umożliwiają efektywne zarządzanie kodem asynchronicznym, eliminując potrzebę skomplikowanych zagnieżdżeń funkcji zwrotnych.
Przykłady zastosowania klasy Promise
- Równoczesne żądania HTTP: W aplikacjach sieciowych można wykorzystać Promise do wykonywania wielu żądań HTTP jednocześnie, np. w celu pobrania danych z różnych API.
- Operacje na plikach: Promise pozwalają na asynchroniczne odczytywanie i zapisywanie plików bez blokowania głównego wątku aplikacji.
- Bazy danych: Możliwe jest wykonywanie równoległych zapytań do bazy danych.
- Procesy zewnętrzne: Promise nadają się do obsługi długotrwałych procesów zewnętrznych.
Implementacja Promise w PHP
W standardowej bibliotece PHP nie znajdziemy jeszcze natywnej klasy Promise, jednak istnieje kilka popularnych bibliotek, które dostarczają tę funkcjonalność, takich jak:
- Guzzle Promises: Rozszerzenie popularnej biblioteki HTTP Guzzle o obsługę Promise.
- ReactPHP: Framework do programowania asynchronicznego, który wspiera pracę z Promise.
- Amp: Narzędzie dedykowane asynchronicznym operacjom w PHP.
Klasa Promise
class Promise
{
private const PENDING = 'pending';
private const FULFILLED = 'fulfilled';
private const REJECTED = 'rejected';
private string $state = self::PENDING;
private mixed $value = null;
private array $onFulfilled = [];
private array $onRejected = [];
public function __construct(?callable $executor = null)
{
if ($executor === null) {
return;
}
try {
$executor(
fn ($value) => $this->resolve($value),
fn ($reason) => $this->reject($reason)
);
} catch (Throwable $e) {
$this->reject($e);
}
}
public function then(?callable $onFulfilled = null, ?callable $onRejected = null): self
{
return new self(function ($resolve, $reject) use ($onFulfilled, $onRejected) {
$this->handle(
function ($value) use ($onFulfilled, $resolve, $reject) {
try {
if ($onFulfilled === null) {
$resolve($value);
return;
}
$result = $onFulfilled($value);
if ($result instanceof self) {
$result->then($resolve, $reject);
} else {
$resolve($result);
}
} catch (Throwable $e) {
$reject($e);
}
},
function ($reason) use ($onRejected, $resolve, $reject) {
try {
if ($onRejected === null) {
$reject($reason);
return;
}
$result = $onRejected($reason);
if ($result instanceof self) {
$result->then($resolve, $reject);
} else {
$resolve($result);
}
} catch (Throwable $e) {
$reject($e);
}
}
);
});
}
public function resolve(mixed $value): void
{
if ($this->state !== self::PENDING)
{
return;
}
if ($value instanceof self)
{
$value->then(
function($v) { $this->resolve($v); },
function($r) { $this->reject($r); }
);
return;
}
$this->state = self::FULFILLED;
$this->value = $value;
foreach ($this->onFulfilled as $callback)
{
$callback($value);
}
$this->onFulfilled = $this->onRejected = [];
}
public function reject(mixed $reason): void
{
if ($this->state !== self::PENDING) {
return;
}
$this->state = self::REJECTED;
$this->value = $reason;
foreach ($this->onRejected as $callback) {
$callback($reason);
}
$this->onFulfilled = $this->onRejected = [];
}
private function handle(callable $onFulfilled, callable $onRejected): void
{
if ($this->state === self::FULFILLED) {
$onFulfilled($this->value);
} elseif ($this->state === self::REJECTED) {
$onRejected($this->value);
} else {
$this->onFulfilled[] = $onFulfilled;
$this->onRejected[] = $onRejected;
}
}
public static function resolveStatic(mixed $value): self
{
$promise = new self();
$promise->resolve($value);
return $promise;
}
public static function rejectStatic(mixed $reason): self
{
$promise = new self();
$promise->reject($reason);
return $promise;
}
public static function all(array $promises): self
{
return new self(function ($resolve, $reject) use ($promises) {
$results = [];
$remaining = count($promises);
if ($remaining === 0) {
$resolve([]);
return;
}
foreach ($promises as $index => $promise) {
$promise->then(
function ($value) use (&$results, &$remaining, $index, $resolve) {
$results[$index] = $value;
$remaining--;
if ($remaining === 0) {
ksort($results);
$resolve($results);
}
},
fn ($reason) => $reject($reason)
);
}
});
}
}
Testy klasy
require 'promise.php';
function assertEqual($expected, $actual, string $label)
{
if ($expected === $actual) {
echo "✅ $label\n";
} else {
echo "❌ $label\n";
echo " Expected: ";
var_dump($expected);
echo " Actual: ";
var_dump($actual);
exit(1);
}
}
/**
* TEST 1: resolve + then
*/
$result = null;
$p = new Promise();
$p->then(function($v) { assertEqual(10, $v, 'Resolve basic'); });
$p->resolve(10);
/**
* TEST 2: reject + catch
*/
$error = null;
$p = new Promise();
$p->then(null, function ($e) { assertEqual('fail', $e, 'Reject basic'); });
$p->reject('fail');
/**
* TEST 3: chainowanie then
*/
$result = null;
Promise::resolveStatic(5)
->then(function ($v) { return $v * 2; })
->then(function ($v) { assertEqual(10, $v, 'Then chaining'); });
/**
* TEST 4: flattening (then zwraca Promise)
*/
$result = null;
Promise::resolveStatic(3)
->then(function ($v) { return new Promise(function ($r) { return $r(1); }); })
->then(function ($v) { assertEqual(1, $v, 'Promise flattening');});
/**
* TEST 5: Promise::all
*/
$result = null;
Promise::all([
Promise::resolveStatic(1),
Promise::resolveStatic(2),
Promise::resolveStatic(3),
])->then(function ($values) { assertEqual([1, 2, 3], $values, 'Promise::all'); });
;
/**
* TEST 6: resolve tylko raz
*/
$count = 0;
$p = new Promise();
$p->then(function($v) use (&$count) { $count++; });
$p->resolve(1);
$p->resolve(2);
assertEqual(1, $count, 'Resolve only once');
echo "\n🎉 Wszystkie testy przeszły poprawnie\n";
Prosty przykład z wykorzystaniem Guzzle Promises
require 'vendor/autoload.php';
use GuzzleHttp\Client;
use GuzzleHttp\Promise;
$client = new Client();
$promise1 = $client->getAsync('https://api.example.com/data1');
$promise2 = $client->getAsync('https://api.example.com/data2');
// Równoczesne uruchomienie obu żądań
$results = Promise\settle([$promise1, $promise2])->wait();
foreach ($results as $result) {
if ($result['state'] === 'fulfilled') {
echo $result['value']->getBody();
} else {
echo "Error: " . $result['reason'];
}
}
W powyższym kodzie równocześnie wykonywane są dwa żądania HTTP, a wyniki są obsługiwane po zakończeniu operacji.
Promise w Koseven
<?php
defined('SYSPATH') or die('No direct script access.');
class Kohana_Promise
{
const PENDING = 00;
const FULFILLED = 10;
const REJECTED = 20;
private $state = self::PENDING;
private $value = null;
private $onFulfilled = [];
private $onRejected = [];
public static function factory(callable $executor = NULL)
{
return new self($executor);
}
public function __construct(callable $executor = null)
{
if ($executor === null)
{
return;
}
try
{
$executor(
function($value) {
$this->resolve($value);
},
function($reason) {
$this->reject($reason);
}
);
} catch (Exception $e) {
$this->reject($e);
}
}
public function then(callable $onFulfilled = null, callable $onRejected = null)
{
return new self(function ($resolve, $reject) use ($onFulfilled, $onRejected) {
$this->handle(
function ($value) use ($onFulfilled, $resolve, $reject) {
try {
if ($onFulfilled === null) {
$resolve($value);
return;
}
$result = $onFulfilled($value);
if ($result instanceof self) {
$result->then($resolve, $reject);
} else {
$resolve($result);
}
} catch (Exception $e) {
$reject($e);
}
},
function ($reason) use ($onRejected, $resolve, $reject) {
try {
if ($onRejected === null) {
$reject($reason);
return;
}
$result = $onRejected($reason);
if ($result instanceof self) {
$result->then($resolve, $reject);
} else {
$resolve($result);
}
} catch (Exception $e) {
$reject($e);
}
}
);
});
}
public function resolve($value)
{
if ($this->state !== self::PENDING) return;
if ($value instanceof self)
{
$value->then(
array($this, 'resolve'),
array($this, 'reject')
);
return;
}
$this->state = self::FULFILLED;
$this->value = $value;
foreach ($this->onFulfilled as $callback)
{
$callback($value);
}
$this->onFulfilled = $this->onRejected = [];
}
public function reject($reason)
{
if ($this->state !== self::PENDING)
{
return;
}
$this->state = self::REJECTED;
$this->value = $reason;
foreach ($this->onRejected as $callback)
{
$callback($reason);
}
$this->onFulfilled = $this->onRejected = [];
}
private function handle(callable $onFulfilled, callable $onRejected)
{
if ($this->state === self::FULFILLED)
{
$onFulfilled($this->value);
} elseif ($this->state === self::REJECTED)
{
$onRejected($this->value);
} else
{
$this->onFulfilled[] = $onFulfilled;
$this->onRejected[] = $onRejected;
}
}
public static function resolveStatic($value)
{
$promise = new self();
$promise->resolve($value);
return $promise;
}
public static function rejectStatic($reason)
{
$promise = new self();
$promise->reject($reason);
return $promise;
}
public static function all(array $promises)
{
return new self(function ($resolve, $reject) use ($promises)
{
$results = [];
$remaining = count($promises);
if ($remaining === 0)
{
$resolve([]);
return;
}
foreach ($promises as $index => $promise)
{
$promise->then(
function ($value) use (&$results, &$remaining, $index, $resolve)
{
$results[$index] = $value;
$remaining--;
if ($remaining === 0) {
ksort($results);
$resolve($results);
}
},
function($reason) { $reject($reason); }
);
}
});
}
}
// End Kohana_Promise
Testy w Koseven
PRZYPADEK 1: Najprostszy Promise (jak w JS)
$promise = new Kohana_Promise(function ($resolve, $reject) {
$resolve(123);
});
$promise->then(function ($value) {
echo $value; // 123
});
PRZYPADEK 2: resolve / reject warunkowo
$isOk = false;
$promise = new Kohana_Promise(function ($resolve, $reject) use ($isOk) {
if ($isOk) {
$resolve('OK');
} else {
$reject('ERROR');
}
});
$promise->then(
function ($value) {
echo "SUCCESS: $value\n";
},
function ($error) {
echo "FAIL: $error\n";
}
);
PRZYPADEK 3: Deferred (bardzo częste w Kohana)
$promise = new Kohana_Promise();
$promise->then(function ($value) {
echo "Got: $value\n";
});
// gdzieś później (np. po DB, curl, event)
$promise->resolve(42);
PRZYPADEK 4: Chaining
Kohana_Promise::resolveStatic(10)
->then(function ($v) {
return $v * 2;
})
->then(function ($v) {
echo $v; // 20
});
PRZYPADEK 5: then zwraca Promise (flattening)
Kohana_Promise::resolveStatic(5)
->then(function ($v) {
return new Kohana_Promise(function ($resolve) use ($v) {
$resolve($v + 1);
});
})
->then(function ($v) {
echo $v; // 6
});
PRZYPADEK 6: Promise::all (np. kilka zapytań)
$p1 = Kohana_Promise::resolveStatic(1);
$p2 = Kohana_Promise::resolveStatic(2);
$p3 = Kohana_Promise::resolveStatic(3);
Kohana_Promise::all([$p1, $p2, $p3])
->then(function ($values) {
print_r($values); // [1, 2, 3]
});
Zalety korzystania z Promise
- Zwiększona wydajność: Możliwość wykonywania wielu operacji jednocześnie.
- Czytelny kod: Eliminacja zagnieżdżeń funkcji zwrotnych (tzw. callback hell).
- Obsługa błędów: Centralne zarządzanie błędami operacji asynchronicznych.
Podsumowanie
Klasa Promise stanowi potężne narzędzie umożliwiające efektywne zarządzanie operacjami asynchronicznymi w PHP. Choć natywna obsługa asynchroniczności nadal jest ograniczona, biblioteki takie jak Guzzle, ReactPHP czy Amp dostarczają potężnych mechanizmów dla programistów. Warto rozważyć ich wykorzystanie w projektach wymagających wysokiej wydajności i responsywności.