Przejdź do głównej treści
Grafika przedstawia ukryty obrazek

Klasa Promise w PHP i jej zastosowanie

Zdjecie zwiazane z 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

  1. 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.
  2. Operacje na plikach: Promise pozwalają na asynchroniczne odczytywanie i zapisywanie plików bez blokowania głównego wątku aplikacji.
  3. Bazy danych: Możliwe jest wykonywanie równoległych zapytań do bazy danych.
  4. 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.

5 lutego 2025 39

Kategorie

programowanie

Tagi

php

Dziękujemy!
()

Powiązane wpisy

Ilustracja tematu Automatyczny motyw Bootstrap 53 na bazie pory dnia
4 stycznia 2025 6 min 71

Automatyczny motyw Bootstrap 5.3 na bazie pory dnia

php
Czytaj więcej
Grafika przedstawia Long Polling vs Short Polling Porwnanie
21 stycznia 2025 5 min 26

Long Polling vs. Short Polling: Porównanie

php
Czytaj więcej
Zdjecie zwiazane z Wyraenia regularne i ich obsuga w PHP oraz JavaScript
1 lutego 2025 5 min 19

Wyrażenia regularne i ich obsługa w PHP oraz JavaScript

php
Czytaj więcej
Wymiana doświadczeń

Masz podobne doświadczenia?

Chętnie poznam Twoją perspektywę i porozmawiam o tym temacie szerzej.

Napisz do mnie

Każda perspektywa może wnieść coś wartościowego do dyskusji.

Twoja prywatność i pliki cookies

  1. Ta strona internetowa wykorzystuje wyłącznie niezbędne pliki cookies, które są wymagane do jej prawidłowego działania – m.in. do poprawnego wyświetlania treści, zapamiętania podstawowych ustawień przeglądarki oraz zapewnienia stabilności serwisu.
  2. Nie stosuję plików cookies w celach marketingowych, reklamowych ani analitycznych.
  3. Strona ma charakter wyłącznie informacyjny i nie zawiera formularzy kontaktowych, rejestracyjnych ani zakupowych, przez które dane mogłyby być przesyłane na serwer.
  4. Nie zbieram danych osobowych podczas zwykłego korzystania z witryny.
  5. Serwis nie korzysta z certyfikatu SSL, jednak ze względu na informacyjny charakter strony nie jest wymagane przesyłanie poufnych danych. Zalecam jednak, aby nigdy nie wpisywać haseł ani danych osobowych na stronach bez szyfrowanego połączenia.
  6. Korzystając z tej strony, wyrażasz zgodę na używanie wyłącznie niezbędnych plików cookies.

Więcej informacji znajdziesz w mojej polityce prywatności.