Przygotowanie do rozmowy kwalifikacyjnej z PHP

Pytania na rozmowę z PHP

15 wybranych pytań rekrutacyjnych dla programistów backendowych PHP, pogrupowanych według poziomu doświadczenia. Wykorzystaj je do powtórzenia podstaw, rozważenia praktycznych kompromisów i wnioskowania na poziomie seniora w kontekście produkcyjnym.

Rozpocznij wywiad AI z PHPKarta kredytowa nie jest wymagana. Dostępna 1 darmowa sesja.
Trening technicznych rozmów rekrutacyjnych po angielskuTryb dla osób, dla których angielski nie jest językiem ojczystym, do ćwiczenia rozmów technicznych.

Pytania dla Juniorów

1Jak system typów PHP rozróżnia słabe i ścisłe typowanie oraz co zmienia declare(strict_types=1)?

PHP zwykle dopuszcza zgodne konwersje skalarne dla zadeklarowanych typów parametrów i wartości zwracanych, na przykład napis liczbowy przekazany do parametru int. declare(strict_types=1) włącza ścisłe kontrole skalarne: niezgodny argument lub wartość zwracana powoduje TypeError, choć int nadal jest akceptowany dla float. Dla argumentów funkcji użytkownika ścisłość określa plik wywołujący; kontrole wartości zwracanych zależą od pliku definiującego funkcję. Wywołania z wewnętrznych funkcji PHP mają własne zastrzeżenia i nie podlegają po prostu ustawieniu strict_types zewnętrznego kodu wywołującego. Dyrektywa musi znajdować się na początku swojego pliku PHP. Nie czyni PHP językiem statycznie typowanym i nie wyłącza konwersji wykonywanych przez operatory w całym języku.

// THREE SEPARATE FILES: save each PHP block under its indicated filename.
// File: lib.php
<?php
function takesInt(int $x): int { return $x; }
?>

// File: weak.php
<?php
require __DIR__ . '/lib.php';
echo takesInt('123'); // 123: coercive caller
?>

// File: strict.php
<?php
declare(strict_types=1);
require __DIR__ . '/lib.php';
try {
    echo takesInt('123');
} catch (TypeError $e) {
    echo 'TypeError';
}
?>
Odpowiedz na to pytanie z trenerem AI

2Wyjaśnij różnicę między == i === w PHP, uwzględniając pułapki związane z napisami liczbowymi, tablicami, obiektami i in_array().

PHP używa w == porównania luźnego, z konwersją typów tam, gdzie ma ona zastosowanie; === wymaga zgodności typów i wartości bez niejawnej konwersji skalarów. Napisy liczbowe mogą być porównywane liczbowo, dlatego nie używaj porównania luźnego, gdy wymagana jest dokładna zgodność uwzględniająca typ. Tablice są równe według ==, jeśli zawierają te same pary klucz/wartość według reguł porównania, niezależnie od kolejności par. === dla tablic wymaga dodatkowo tej samej kolejności oraz zgodności typów kluczy i wartości. == dla obiektów porównuje właściwości obiektów tej samej klasy; === wymaga tej samej instancji. in_array domyślnie porównuje luźno; przekaż true jako trzeci argument, aby użyć kontroli ścisłej. Zachowanie luźnego porównywania liczb i napisów zmieniło się w PHP 8, więc unikaj opierania się na historycznych przykładach konwersji bez wskazania wersji.

<?php
var_dump(123 == '123');      // loose: numeric string is converted
var_dump(123 === '123');     // strict: int !== string

$values = [0, 1, 2];
var_dump(in_array('0', $values));       // loose
var_dump(in_array('0', $values, true)); // strict
Odpowiedz na to pytanie z trenerem AI

3Czym różnią się mixed, void i never jako typy zwracane w PHP i jakie zapewniają gwarancje?

mixed dopuszcza dowolną zwracaną wartość, w tym null. void deklaruje, że funkcja nie zwraca znaczącej wartości: może dojść do końca lub użyć return;, ale nie może zwrócić wyrażenia. Zwykłe wywołanie funkcji void daje null. never, dostępne od PHP 8.1, oznacza, że funkcja nigdy normalnie nie oddaje sterowania wywołującemu. Może zgłosić wyjątek, zakończyć wykonanie przez exit albo pozostać w nieskończonej pętli; nie musi ostatecznie zakończyć procesu. Funkcja never nie może zawierać instrukcji return. Kluczowa różnica polega na tym, że funkcja void może zakończyć się normalnie i pozwolić wywołującemu kontynuować, natomiast funkcja never nie ma normalnej kontynuacji.

<?php
function parse(string $raw): mixed {
    return json_decode($raw, true);
}

function logMessage(string $message): void {
    error_log($message);
    return; // allowed
}

function fail(string $message): never {
    throw new RuntimeException($message);
}

logMessage('starting');
fail('stop');
echo 'unreachable';
Odpowiedz na to pytanie z trenerem AI

4Czym jest iterable w PHP i jak wiąże się z array, Traversable, Iterator i Generator?

iterable oznacza array|Traversable. Generator implementuje Iterator, który rozszerza Traversable, dlatego spełnia iterable. Klasy użytkownika zapewniają zachowanie Traversable przez Iterator lub IteratorAggregate, zamiast bezpośrednio implementować Traversable w konkretnej klasie. Iterator udostępnia metody sterujące iteracją; IteratorAggregate dostarcza iterator. Funkcja generatora zawierająca yield zwraca Generator. Istotna różnica polega na tym, że foreach może również przeglądać widoczne właściwości zwykłego obiektu, który nie implementuje Traversable, lecz nie oznacza to, że obiekt spełnia deklarację parametru iterable.

function printItems(iterable $items): void {
    foreach ($items as $item) {
        echo $item, PHP_EOL;
    }
}

printItems(['a', 'b']);

function letters(): Generator {
    yield 'x';
    yield 'y';
}

printItems(letters());
Odpowiedz na to pytanie z trenerem AI

5Jakie są reguły widoczności public, protected i private w PHP oraz jak współdziałają z dziedziczeniem i traitami?

Składowe public są dostępne wszędzie, gdzie można uzyskać dostęp do obiektu lub klasy. Składowe protected są dostępne wewnątrz deklarującej klasy i powiązanej hierarchii klas nadrzędnych i pochodnych, ale nie dla dowolnego kodu zewnętrznego. Składowe private są dostępne w klasie, która je deklaruje; podklasa nie może używać prywatnej składowej rodzica jako swojego odziedziczonego interfejsu. Zwykła metoda nadpisująca nie może zawężać odziedziczonej widoczności. Konstruktory mają jednak szczególne reguły dziedziczenia i nie należy traktować ich jako bezwarunkowego przykładu tej zasady. Składowe traita są włączane do korzystającej klasy i podlegają jej regułom widoczności. Adaptacja as może tworzyć alias, zmieniać widoczność metody traita albo robić obie rzeczy; samo dodanie aliasu nie usuwa pierwotnej nazwy.

<?php
class Base {
    public function a() {}
    protected function b() {}
    private function c() {}
}

trait Logs {
    public function log(string $message): void {}
}

class Child extends Base {
    use Logs { log as protected writeLog; }

    public function test(): void {
        $this->a();              // OK
        $this->b();              // OK
        // $this->c();           // Not accessible: private to Base
        $this->writeLog('ok');   // OK inside class, protected alias
    }
}

$child = new Child();
$child->a();
// $child->b();           // Not accessible from outside
// $child->writeLog('x'); // Not accessible from outside
Odpowiedz na to pytanie z trenerem AI

6Jak działają traity w PHP i jak rozwiązuje się konflikty, gdy kilka traitów definiuje tę samą metodę?

Traity zapewniają poziome ponowne wykorzystanie kodu: klasa importuje ich składowe przez use, a sam trait nie może być instancjonowany. Metoda zadeklarowana w klasie ma pierwszeństwo przed metodami traitów, które mają pierwszeństwo przed metodami odziedziczonymi z klasy nadrzędnej. Jeśli kilka traitów dostarcza tę samą metodę i konfliktu nie rozwiązuje już metoda klasy, użyj insteadof, aby wybrać implementację. Użyj as, aby nadać metodzie traita dodatkową nazwę i opcjonalnie inną widoczność. Alias nie zmienia nazwy ani nie usuwa pierwotnej metody. Rozwiązywanie kolizji i zachowanie czytelności powstałego interfejsu publicznego to osobne zadania.

<?php
trait JsonLogger {
    public function log(string $message): void {
        echo "json:$message\n";
    }
}

trait FileLogger {
    public function log(string $message): void {
        echo "file:$message\n";
    }
}

class Service {
    use JsonLogger, FileLogger {
        JsonLogger::log insteadof FileLogger;
        FileLogger::log as logToFile;
        JsonLogger::log as protected logJson;
    }
}

$service = new Service();
$service->log('hello');
$service->logToFile('hello');
// $service->logJson('hello'); // not accessible from outside; protected alias
Odpowiedz na to pytanie z trenerem AI

7Czym są enumy (typy wyliczeniowe) w PHP, w tym czyste enumy i enumy ze wspieranymi wartościami (backed enums), i jak poprawiają one modelowanie dziedziny (domain modeling)?

Enumy (typy wyliczeniowe) w PHP definiują zamknięty zbiór nazwanych przypadków dla danego typu, takich jak statusy zamówień lub role użytkowników. Czyste enumy mają tylko nazwane przypadki; enumy ze wspieranymi wartościami (backed enums) przypisują każdemu przypadkowi unikalną wartość ciągu znaków (string) lub liczbę całkowitą (int), co jest przydatne do przechowywania, użycia w API (Application Programming Interface) lub interoperacyjności. Enumy poprawiają modelowanie dziedziny (domain modeling) poprzez zastępowanie wrażliwych surowych ciągów znaków, liczb całkowitych lub stałych wartościami bezpiecznymi typowo i samodokumentującymi się. Enumy w PHP mogą również mieć metody i implementować interfejsy; wszystkie enumy obsługują listowanie swoich przypadków, a enumy ze wspieranymi wartościami obsługują konwersję do i z ich wartości skalarnych.

enum OrderState
{
    case Draft;
    case Paid;
    case Shipped;
}

enum PaymentStatus: string
{
    case Pending = 'pending';
    case Complete = 'complete';
    case Failed = 'failed';
}

$status = PaymentStatus::tryFrom('complete');
var_dump($status === PaymentStatus::Complete);
Odpowiedz na to pytanie z trenerem AI

Pytania dla Midów

8Jak typy skalarne, nullable, unie, przecięcia i DNF wyrażają kontrakty interfejsów PHP i kiedy stosować każdy z nich?

Deklaracje PHP opisują akceptowane i zwracane wartości. Deklaracje skalarne, takie jak int, float, string i bool, wybierają kategorię wartości; dopuszczalność zgodnych konwersji skalarnych zależy także od strict_types i kontekstu. ?T lub T|null dopuszcza null. Unie, dostępne od PHP 8.0, dopuszczają alternatywy, na przykład int|string. Czyste przecięcia, dostępne od PHP 8.1, wymagają od obiektu spełnienia wszystkich wymienionych typów klas lub interfejsów, na przykład Cacheable&JsonSerializable; int&string nie jest poprawnym przecięciem. Typy DNF (dysjunkcyjna postać normalna), dostępne od PHP 8.2, łączą alternatywy i przecięcia ujęte w nawiasy, na przykład (Cacheable&JsonSerializable)|string. Wybieraj najwęższy zrozumiały kontrakt opisujący prawidłowe dane wejściowe, zamiast dodawać zbędne alternatywy.

<?php
interface Cacheable {}

class Report implements Cacheable, JsonSerializable {
    public function jsonSerialize(): mixed { return []; }
}

function findName(int $id): ?string {
    return $id > 0 ? 'Alice' : null;
}

function normalize(int|string $id): string {
    return (string) $id;
}

function store(Cacheable&JsonSerializable $item): void {
    // must satisfy both interfaces
}

function handle((Cacheable&JsonSerializable)|string $input): void {
    // either an object satisfying both contracts, or a string
}
Odpowiedz na to pytanie z trenerem AI

9Czym różnią się callable, Closure, wartości wywoływalne pierwszej klasy, obiekty wywoływalne i tablice callable w PHP?

callable opisuje wartości, które można wywołać, w tym napisy z nazwami funkcji, obiekty Closure, referencje do dostępnych metod oraz obiekty z __invoke. Closure jest konkretnym obiektem wywoływalnym: może pochodzić z funkcji anonimowej albo nazwanej funkcji lub metody przekształconej w Closure. Składnia wartości wywoływalnych pierwszej klasy, taka jak strlen(...) lub $object->method(...), tworzy Closure bez wywoływania celu. Tablica callable [$object, 'method'] wskazuje dostępną metodę instancji; [ClassName::class, 'method'] wymaga we współczesnym PHP odpowiedniej dostępnej metody statycznej. is_callable sprawdza możliwość wywołania we właściwym zakresie widoczności. callable jest szerokim kontraktem, a Closure konkretną reprezentacją.

class Greeter {
    public function hello(string $name): string { return "Hello $name"; }
    public function __invoke(string $name): string { return "Hi $name"; }
}

$g = new Greeter();

$closure = fn(string $name) => "Hey $name";
$arrayCallable = [$g, 'hello'];
$firstClass = $g->hello(...);
$invokable = $g;

foreach ([$closure, $arrayCallable, $firstClass, $invokable] as $cb) {
    echo $cb('Sam'), PHP_EOL;
}
Odpowiedz na to pytanie z trenerem AI

10Jak PHP (Hypertext Preprocessor) obsługuje przepełnienie liczby całkowitej (integer overflow), precyzję liczb zmiennoprzecinkowych (floating-point precision) i dokładne obliczenia dziesiętne?

Liczby całkowite w PHP mają rozmiar zależny od platformy, z ograniczeniami ujawnionymi przez stałe takie jak `PHP_INT_MAX`. PHP nie zachowuje automatycznie natywnych wyników całkowitych jako liczb całkowitych o dowolnej precyzji po przepełnieniu; arytmetyka liczb całkowitych powodująca przepełnienie może stać się `float` lub w inny sposób stracić dokładną semantykę liczb całkowitych. Liczby zmiennoprzecinkowe (floats) w PHP są binarnymi wartościami zmiennoprzecinkowymi, dlatego wiele ułamków dziesiętnych, takich jak `0.1`, jest przybliżonych. Do dokładnych obliczeń dziesiętnych lub pieniężnych należy unikać liczb zmiennoprzecinkowych i stosować precyzyjne podejście, takie jak całkowite jednostki pomniejsze (np. grosze zamiast złotówek), gdy jest to właściwe, lub narzędzia do obliczeń dziesiętnych o stałej/dowolnej precyzji, takie jak BCMath. GMP jest przydatne do arytmetyki liczb całkowitych o dowolnej precyzji.

var_dump(0.1 + 0.2 === 0.3);
printf('%.17f\n', 0.1 + 0.2);
Odpowiedz na to pytanie z trenerem AI

11Wyjaśnij referencje (&) w PHP, kopiowanie przy zapisie (copy-on-write) oraz to, jak referencje mogą prowadzić do nieoczekiwanych skutków ubocznych.

Zmienne PHP są wewnętrznie reprezentowane przez zvals. W przypadku wartości takich jak tablice i ciągi znaków, PHP stosuje kopiowanie przy zapisie (copy-on-write): przypisanie `$b = $a` zazwyczaj współdzieli tę samą podstawową wartość, dopóki jedna ze zmiennych nie zostanie zmodyfikowana, w którym to momencie PHP ją rozdziela/kopiuje. Referencja utworzona za pomocą `&` sprawia, że zmienne stają się aliasami do tego samego kontenera, więc zmiana jednej zmienia drugą i może omijać zachowanie rozdzielenia, którego ludzie oczekują po normalnym przypisaniu. Przekazywanie przez referencję pozwala funkcji modyfikować zmienną wywołującego. Referencje mogą powodować zaskakujące skutki uboczne, zwłaszcza z `foreach ($array as &$value)`: zmienna pętli pozostaje referencją do ostatniego elementu po zakończeniu pętli, więc ponowne jej użycie później może przypadkowo nadpisać ten element, chyba że wykonasz `unset($value)`.

$a = [1, 2];
$b = $a;      // shared until modification
$b[0] = 99;   // separates; $a is unchanged
var_dump($a, $b);

$c =& $a;     // alias/reference
$c[0] = 42;   // modifies $a too
var_dump($a, $c);
Odpowiedz na to pytanie z trenerem AI

12Jak działają uchwyty obiektów, przypisanie, klonowanie (`clone`), metoda magiczna `__clone`, płytka kopia i głęboka kopia w PHP?

W PHP zmienna obiektu przechowuje uchwyt do obiektu, a nie pełną kopię obiektu. Przypisanie `$b = $a` kopiuje uchwyt, więc obie zmienne odwołują się do tego samego obiektu; modyfikacja obiektu za pośrednictwem którejkolwiek zmiennej jest widoczna przez drugą, bez używania `&`. `clone $a` tworzy nową instancję obiektu z płytką kopią właściwości oryginalnego obiektu. Jeśli właściwość jest sama w sobie obiektem, właściwość sklonowanego obiektu nadal wskazuje na ten sam zagnieżdżony obiekt, chyba że jawnie go sklonujesz. Metoda `__clone()` jest wywoływana na nowym obiekcie po płytkiej kopii i jest miejscem do resetowania identyfikatorów, odłączania zasobów lub wykonywania głębokiej kopii zagnieżdżonych obiektów. Głęboka kopia oznacza rekurencyjne kopiowanie zagnieżdżonych, mutowalnych obiektów, które nie powinny być współdzielone.

class Address {
    public function __construct(public string $city) {}
}

class User {
    public function __construct(public string $name, public Address $address) {}

    public function __clone() {
        $this->address = clone $this->address; // make nested object independent
    }
}

$u1 = new User('Ann', new Address('Paris'));
$u2 = $u1;          // same object handle
$u2->name = 'Beth';
echo $u1->name, PHP_EOL;

$u3 = clone $u1;    // new User object; __clone deep-clones address
$u3->address->city = 'Rome';
echo $u1->address->city, PHP_EOL;
echo $u3->address->city, PHP_EOL;
Odpowiedz na to pytanie z trenerem AI

Pytania dla Seniorów

13Jak rozwiązałbyś problem wyczerpania puli połączeń do bazy danych i zaprojektowałbyś zarządzanie połączeniami dla PHP-FPM i długo działających workerów?

Aby rozwiązać problem wyczerpania połączeń, porównałbym limity połączeń do bazy danych i bieżące sesje z całkowitą jednoczesnością PHP: każdy aktywny proces potomny PHP-FPM może utrzymywać własne połączenie z bazą danych, a wiele pul aplikacji, hostów, zadań cron, procesów obsługujących kolejki, migracji i wdrożeń zwielokrotnia zapotrzebowanie. Sprawdziłbym widoki sesji bazy danych, stany połączeń, sesje bezczynne w transakcji, wolne zapytania, oczekiwania na blokady, logi błędów, metryki puli oraz ostatnie zmiany w ruchu lub liczbie workerów. Dla PHP-FPM, należy utrzymywać `max_children` i jednoczesność na hosta w ramach budżetu bazy danych, używać krótkotrwałych transakcji/połączeń o zasięgu żądania, zatwierdzać lub wycofywać transakcje szybko i unikać utrzymywania połączeń podczas wolnych wywołań zewnętrznych. Trwałe połączenia PDO (Persistent PDO connections) mogą zredukować narzut związany z nawiązywaniem połączeń, ale mogą przypiąć wiele bezczynnych połączeń do każdego procesu FPM i zachować stan sesji, dlatego wymagają ostrożności. Puler lub proxy, takie jak PgBouncer, może multipleksować wielu klientów PHP na mniejszą liczbę połączeń z serwerem bazy danych, jeśli aplikacja jest kompatybilna. Długo działające workery wymagają jawnego zarządzania cyklem życia połączenia: kontrole zdrowia, ponowne nawiązanie połączenia po przekroczeniu limitu czasu bezczynności/restartach/przełączeniu awaryjnym/forkach, resetowanie stanu sesji i ponawianie tylko bezpiecznych lub idempotentnych operacji.

Connection-budget example:
- 6 application servers
- 2 PHP-FPM pools on EACH server
- pm.max_children = 40 for EACH pool
- Assume at most 1 database connection per child

Potential request connections = 6 * 2 * 40 = 480.
If 40 were the TOTAL children per server across both pools, it would be 240.
Budget separately for queue workers, cron, deployments and administrators.
A limit of 300 database connections cannot accommodate 480 simultaneous connections.
Odpowiedz na to pytanie z trenerem AI

14Jak obsłużyć repliki do odczytu, rozdzielanie odczytów i zapisów, opóźnienie replikacji i przełączenie po awarii głównej bazy danych w PHP?

Rozdzielaj połączenia do bazy głównej i replik według znaczenia operacji, a nie wyłącznie tego, czy SQL zaczyna się od SELECT: odczyty zakładające blokady, zapisy oraz transakcje wymagające spójności bazy głównej muszą z niej korzystać. Transakcje tylko do odczytu mogą używać odpowiedniej repliki, jeśli pozwalają na to ich wymagania spójności. Replikacja asynchroniczna może mieć opóźnienie. Ustawienie sticky w Laravel kieruje odczyty następujące po zapisie do połączenia zapisującego w obrębie bieżącego żądania; przypięcie do bazy głównej na stały okres między żądaniami jedynie ogranicza problem, bez bezwarunkowej gwarancji odczytania własnych zapisów. Aby ją uzyskać, czytaj z bazy głównej lub czekaj, aż replika zastosuje wymaganą pozycję zapisu, z limitem czasu i przejściem na bazę główną po jego przekroczeniu. Zachowanie proxy jest różne: RDS Proxy używa osobnych punktów końcowych dla odczytu i zapisu i nie jest ogólnym automatycznym rozdzielaczem odczytów i zapisów dla każdej instrukcji. Przy przełączeniu po awarii odrzucaj nieaktualne połączenia i łącz się ponownie z ograniczonym wydłużaniem odstępów. Utrata odpowiedzi na commit pozostawia wynik niepewny: nie powtarzaj w ciemno nieidempotentnych zapisów; stosuj klucze idempotencji lub najpierw ustal wynik.

<?php
// config/database.php
return [
    'mysql' => [
        'read' => [
            'host' => [env('DB_REPLICA_HOST_1'), env('DB_REPLICA_HOST_2')],
        ],
        'write' => [
            'host' => [env('DB_PRIMARY_HOST')],
        ],
        'sticky'    => true,
        'driver'    => 'mysql',
        'database'  => env('DB_DATABASE'),
        'username'  => env('DB_USERNAME'),
        'password'  => env('DB_PASSWORD'),
        'charset'   => 'utf8mb4',
        'collation' => 'utf8mb4_unicode_ci',
    ],
];
Odpowiedz na to pytanie z trenerem AI

15Jak bezpiecznie wdrażać migracje bazy danych i zmiany schematu w aplikacjach PHP bez przestoju lub z minimalnym przestojem?

Stosuj zmiany polegające na rozszerzaniu i późniejszym usuwaniu starych struktur, które tolerują starą i nową wersję aplikacji podczas stopniowego wdrożenia. Najpierw dodaj zgodne struktury, następnie wdróż kod korzystający z obu reprezentacji. Jeśli potrzebny jest podwójny zapis, zapewnij jego spójność i zaprojektuj wznawialne uzupełnianie danych historycznych z ograniczoną szybkością, które nie nadpisuje nowszych zapisów. Zweryfikuj skopiowane dane i przełącz odczyty przed usunięciem starych ścieżek kodu. DDL (język definicji danych) wykonywany online ogranicza pewne ryzyka blokowania, ale nie zawsze jest pozbawiony blokad: sprawdź blokady właściwe danej bazie i wersji, ograniczenia transakcji, zużycie zasobów oraz wpływ na replikację. Na przykład CREATE INDEX CONCURRENTLY w PostgreSQL ma ograniczenia i po niepowodzeniu może pozostawić nieprawidłowy indeks. Usuń stare kolumny dopiero po wyłączeniu starych instancji aplikacji i zakończeniu wymaganego okresu możliwości wycofania wdrożenia. Po destrukcyjnym usunięciu starych struktur powrót do kodu potrzebującego usuniętych danych nie jest już automatycznie bezpieczny.

<?php
// Step 1: Migration (Expand) -> Add new column as nullable
Schema::table('users', function (Blueprint $table) {
    $table->string('full_name')->nullable()->after('name');
});

// Step 2: Code deployment -> Write to both, read with fallback
class User extends Model {
    public function setNameAttribute($value) {
        $this->attributes['name'] = $value;
        $this->attributes['full_name'] = $value; // Dual write
    }
}

// Step 3: Background worker backfills existing rows in small batches
// Step 4: Code deployment -> Read/write exclusively from 'full_name'
// Step 5: Migration (Contract) -> Drop legacy 'name' column
Odpowiedz na to pytanie z trenerem AI