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 37

Kategorie

programowanie

Tagi

php

Dziękujemy!
()

Powiązane wpisy


Informacja o cookies

Moja strona internetowa wykorzystuje wyłącznie niezbędne pliki cookies, które są wymagane do jej prawidłowego działania. Nie używam ciasteczek w celach marketingowych ani analitycznych. Korzystając z mojej strony, wyrażasz zgodę na stosowanie tych plików. Możesz dowiedzieć się więcej w mojej polityce prywatności.