PHP-haastatteluun valmistautuminen

PHP-haastattelukysymykset

15 valittua PHP-taustaohjelmistokehityksen haastattelukysymystä ryhmiteltynä kokemustason mukaan. Käytä niitä perusasioiden, käytännön kompromissien ja senioritason tuotantopäätösten kertaamiseen.

Aloita PHP-tekoälyhaastatteluEi vaadi luottokorttia. 1 ilmainen sessio saatavilla.
Teknisten haastattelujen harjoittelu englanniksiTila, jossa ei-äidinkieliset puhujat voivat harjoitella teknisen haastattelun läpäisemistä.

Junior-tason kysymykset

1Miten PHP:n tyyppijärjestelmä erottaa heikon ja tiukan tyypityksen, ja mitä muutoksia tapahtuu, kun `declare(strict_types=1)` otetaan käyttöön?

PHP on oletuksena heikko/pakottava skalaarityyppien määrittelyissä: kun funktio määrittelee skalaarityyppisiä parametreja tai palautustyyppejä, PHP voi muuntaa yhteensopivia arvoja, kuten numeerisia merkkijonoja numeroiksi, sen sijaan että hylkäisi ne. `declare(strict_types=1)` tekee skalaarityyppien tarkistuksesta tiukan kyseisessä tiedostossa, joten yhteensopimattomia skalaariarvoja ei pakoteta ja `TypeError` heitetään. Direktiivi on tiedostokohtainen: käyttäjän määrittelemien funktioiden argumenttien osalta kutsujan tiedosto ohjaa tiukkuutta; palautuslausekkeiden osalta tiedosto, jossa funktio on määritelty, ohjaa paluuarvon tiukkuutta. Se ei tee PHP:stä globaalisti tiukkaa tai poista kaikkia muunnoksia kielessä.

<?php
// lib.php
function takesInt(int $x): int {
    return $x;
}

// weak.php, no strict_types
echo takesInt('123'); // coerces string to int

// strict.php
declare(strict_types=1);
echo takesInt('123'); // TypeError
Kokeile vastata tähän kysymykseen tekoälyvalmentajan kanssa

2Selitä ero `==` ja `===` välillä PHP:ssä, mukaan lukien sudenkuopat numeeristen merkkijonojen, taulukoiden, olioiden ja `in_array()`-funktion kanssa.

`==` suorittaa löyhän vertailun, joten PHP voi jongleerata tyyppejä ennen vertailua; `===` suorittaa tarkan vertailun, vaatien sekä saman arvon että saman tyypin ilman tyyppimuunnoksia. Numeeriset merkkijonot ovat yleinen sudenkuoppa, koska niitä voidaan käsitellä numeroina löyhissä vertailuissa. Taulukoilla ja olioilla on omat vertailusääntönsä: taulukoita verrataan avaimien ja arvojen perusteella, kun taas olioiden `==` vertaa saman luokan olioita ominaisuuksien perusteella ja olioiden `===` vaatii tismalleen saman instanssin. `in_array()` käyttää oletuksena löyhää vertailua, joten välitä `true` kolmantena argumenttina, kun tyyppien on täsmättävä.

<?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
Kokeile vastata tähän kysymykseen tekoälyvalmentajan kanssa

3Miten `mixed`, `void` ja `never` eroavat PHP:n palautustyyppeinä, ja mitä takuita ne tarjoavat?

Palautustyyppeinä `mixed`, `void` ja `never` antavat erilaisia lupauksia. `mixed` tarkoittaa, että funktio voi palauttaa minkä tahansa arvon, mukaan lukien `null`. `void` tarkoittaa, että funktion ei ole tarkoitus palauttaa mielekästä arvoa; se voi käyttää `return;`-lausetta, mutta ei voi palauttaa lauseketta. `never` tarkoittaa, että funktio ei palaa normaalisti kutsujaansa: sen on aina heitettävä poikkeus, poistuttava tai muutoin päätettävä ohjausvuon. `never` on vahvempi kuin `void`, koska suoritus ei voi jatkua `never`-funktion normaalin kutsun jälkeen.

<?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';
Kokeile vastata tähän kysymykseen tekoälyvalmentajan kanssa

4Mikä on `iterable` PHP:ssä, ja miten se liittyy `array`-tyyppiin, `Traversable`-rajapintaan, `Iterator`-rajapintaan ja `Generator`-objektiin?

PHP:ssä `iterable` on tyyppimäärittely, joka tarkoittaa ”`array`ta tai objektia, joka toteuttaa `Traversable`-rajapinnan”. `Traversable` on perustason merkintärajapinta (`marker interface`) objekteille, joita `foreach`-rakenne voi käyttää; käyttäjäkoodi (`userland code`) toteuttaa yleensä `Iterator`in tai `IteratorAggregate`n, jotka laajentavat `Traversable`a. `Generator` on generaattorifunktion palauttama objekti, joka käyttää `yield`iä; se toteuttaa `Iterator`in, joten se on myös `Traversable` ja hyväksytään kaikkialla, missä `iterable`a vaaditaan.

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());
Kokeile vastata tähän kysymykseen tekoälyvalmentajan kanssa

5Mitkä ovat `public`-, `protected`- ja `private`-näkyvyyssäännöt PHP:ssä, ja miten ne toimivat yhdessä periytymisen ja traitien kanssa?

PHP:ssä `public`-jäsenet ovat saavutettavissa mistä tahansa, `protected`-jäsenet ovat saavutettavissa deklaroivasta luokasta ja alaluokista, ja `private`-jäsenet ovat saavutettavissa vain luokasta, joka ne on määrittänyt. Periytymisessä alaluokat voivat käyttää perittyjä `public`- ja `protected`-jäseniä, mutta vanhemman `private`-jäsenet eivät ole saavutettavissa perittynä API-rajapintana (Application Programming Interface). Metodeja ylikirjoitettaessa lapsi ei yleensä voi vähentää näkyvyyttä; esimerkiksi `public`-vanhempimetodin on pysyttävä `public`ina. Traitit yhdistetään käyttävään luokkaan, ja trait-jäsenet noudattavat PHP:n näkyvyyssääntöjä. Trait-metodin näkyvyyttä voidaan myös mukauttaa tai aliasoida `as`-avainsanalla traittia tuotaessa.

<?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
Kokeile vastata tähän kysymykseen tekoälyvalmentajan kanssa

6Miten traitit toimivat PHP:ssä, ja miten ristiriidat ratkaistaan, kun useampi trait määrittelee saman metodin?

Traitit ovat PHP:n mekanismi horisontaaliseen koodin uudelleenkäyttöön: luokka voi käyttää yhtä tai useampaa traitia, ja traitin jäsenet sisällytetään luokkaan. Traitit eivät ole itsenäisiä luokkia, eikä niitä voi instansioida suoraan. Jos useat traitit tarjoavat samannimisiä metodeja, PHP vaatii eksplisiittisen ristiriitojen ratkaisun. `insteadof`-operaattori valitsee, mikä traitin metodi käytetään ristiriitaisessa nimessä. `as`-operaattorilla voidaan luoda alias trait-metodille ja myös muuttaa tuodun metodin näkyvyyttä. Luokkien metodit ylikirjoittavat trait-metodit, ja trait-metodit menevät perittyjen yliluokan metodien edelle.

<?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
Kokeile vastata tähän kysymykseen tekoälyvalmentajan kanssa

7Mitä ovat PHP-luetelmointityypit (enums), mukaan lukien puhtaat ja tuetut luetelmointityypit, ja miten ne parantavat toimialueen mallinnusta?

PHP-luetelmointityypit (enums) määrittelevät suljetun joukon nimettäviä tapauksia tietylle tyypille, kuten tilausten tilat tai käyttäjäroolit. Puhtaissa luetelmointityypeissä on vain nimettävät tapaukset; tuetut luetelmointityypit antavat jokaiselle tapaukselle yksilöllisen merkkijono- tai kokonaisluku-arvon, mikä on hyödyllistä tallennusta, API-rajapintoja tai yhteentoimivuutta varten. Luetelmointityypit parantavat toimialueen mallinnusta korvaamalla hauraat raa'at merkkijonot, kokonaisluvut tai vakiot tyyppiturvallisilla, itseään dokumentoivilla arvoilla. PHP-luetelmointityypeillä voi olla myös metodeja ja ne voivat toteuttaa rajapintoja; kaikki luetelmointityypit tukevat tapaustensa luettelointia, ja tuetut luetelmointityypit tukevat muuntamista skalaariarvoihinsa ja niistä takaisin.

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);
Kokeile vastata tähän kysymykseen tekoälyvalmentajan kanssa

Middle-tason kysymykset

8Miten skalaari-, nullable-, unioni-, intersektio- ja DNF (Disjunctive Normal Form) -tyypit ilmaisevat PHP:n API (Application Programming Interface) -sopimuksia, ja milloin kutakin tulisi käyttää?

PHP:n tyyppimääritykset kuvaavat arvoja, jotka API (Application Programming Interface) hyväksyy tai palauttaa. Skalaarityyppejä, kuten `int`, `float`, `string` ja `bool`, käytetään, kun odotetaan täsmälleen yhtä primitiivityyppiä. Nullable-tyypit, jotka kirjoitetaan `?T` tai `T|null`, sallivat joko tyypin `T` arvon tai `null`-arvon. Unioni-tyypit, kuten `A|B`, sallivat yhden useista vaihtoehdoista. Intersektio-tyypit, kuten `A&B`, edellyttävät, että arvo täyttää kaikki luetellut tyypit, yleensä useita rajapintoja. DNF (Disjunctive Normal Form) -tyypit yhdistävät unioneja ja intersektioita, esimerkiksi `(A&B)|C`, ilmaistakseen monimutkaisempia sopimuksia. Kutakin tulisi käyttää, kun se vastaa todellista API:n lupausta, välttäen tarpeettoman laajoja tai hämmentäviä tyyppejä.

<?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
}
Kokeile vastata tähän kysymykseen tekoälyvalmentajan kanssa

9Mitä eroa on `callable`-tyypillä, `Closure`-oliolla, ensiluokkaisilla kutsumakohteilla (first-class callables), kutsumiskelpoisilla olioilla (invokable objects) ja `callable`-taulukoilla PHP:ssä?

`callable` on laaja tyyppi/sopimus mille tahansa arvolle, jonka PHP voi kutsua, kuten funktionimen sisältävä merkkijono, `Closure`-olio, kutsumiskelpoinen olio tai metodiviittausta sisältävä taulukko, kuten `[$object, 'method']` tai `[ClassName::class, 'method']`. `Closure` on konkreettinen anonyymi funktio-olio ja se on itsessään kutsumiskelpoinen. Ensiluokkainen kutsumakohteen syntaksi, kuten `$obj->method(...)` tai `strlen(...)`, luo `Closure`-olion kutsuttavasta lausekkeesta. Kutsumiskelpoinen olio on kutsumiskelpoinen, koska se määrittelee `__invoke()`-metodin. `callable`-taulukot ovat perinteinen kaksoiselementtimetodiviittausmuoto. `is_callable()`-funktiota voidaan käyttää tarkistamaan kutsuttavuus ajonaikana.

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;
}
Kokeile vastata tähän kysymykseen tekoälyvalmentajan kanssa

10Miten PHP käsittelee kokonaislukujen ylivuotoa, liukulukutarkkuutta ja tarkkoja desimaalilaskelmia?

PHP:n kokonaisluvut ovat alustariippuvaisia, ja niiden rajat ilmoitetaan vakioilla, kuten `PHP_INT_MAX`. PHP ei automaattisesti pidä natiiveja kokonaislukutuloksia mielivaltaisen tarkkuuden kokonaislukuina ylivuodon jälkeen; ylivuotoa aiheuttava kokonaislukuaritmetiikka voi muuttua `float`-tyyppiseksi tai muuten menettää tarkan kokonaislukusemantiikan. PHP:n liukuluvut ovat binäärisiä liukulukuja, joten monet desimaalimurtoluvut, kuten `0.1`, ovat likimääräisiä. Tarkkoja desimaali- tai rahalaskelmia varten vältä liukulukuja ja käytä tarkkaa lähestymistapaa, kuten kokonaislukupohjaisia pienimpiä yksiköitä (esim. sentit) tarvittaessa, tai kiinteän/mielivaltaisen tarkkuuden desimaalityökaluja, kuten BCMathia. GMP on hyödyllinen mielivaltaisen tarkkuuden kokonaislukuaritmetiikassa.

var_dump(0.1 + 0.2 === 0.3);
printf('%.17f\n', 0.1 + 0.2);
Kokeile vastata tähän kysymykseen tekoälyvalmentajan kanssa

11Selitä PHP:n viitteet (`&`), copy-on-write-mekanismi ja miten viitteet voivat aiheuttaa odottamattomia sivuvaikutuksia.

PHP-muuttujat esitetään sisäisesti `zval`-rakenteilla. Arvojen, kuten taulukoiden ja merkkijonojen, kohdalla PHP käyttää copy-on-write-menetelmää: `$b = $a` -sijoitus jakaa yleensä saman pohjalla olevan arvon, kunnes toista muuttujaa muutetaan, jolloin PHP erottaa/kopioi sen. Merkillä `&` luotu viite tekee muuttujista aliaksia samaan säilöön, joten toisen muuttaminen muuttaa myös toista ja voi ohittaa erottelukäyttäytymisen, jota ihmiset odottavat normaalista sijoituksesta. Viitteenä välittäminen antaa funktion muokata kutsujan muuttujaa. Viitteet voivat aiheuttaa yllättäviä sivuvaikutuksia, erityisesti `foreach ($array as &$value)` -silmukan kanssa: silmukkamuuttuja jää viitteeksi viimeiseen elementtiin silmukan jälkeen, joten sen uudelleenkäyttö myöhemmin voi vahingossa ylikirjoittaa kyseisen elementin, ellet kutsu `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);
Kokeile vastata tähän kysymykseen tekoälyvalmentajan kanssa

12Miten olion käsittelyviittaukset, sijoitus (assignment), kloonaus (clone), `__clone`-metodi, matala kopio (shallow copy) ja syvä kopio (deep copy) toimivat PHP:ssä?

PHP:ssä olion muuttuja tallentaa olioon osoittavan viittauksen, ei täydellistä kopioita oliosta. Kun suoritetaan sijoitus kuten `$b = $a`, kopioidaan viittaus, jolloin molemmat muuttujat osoittavat samaan olioon. Olion muuttaminen kummankin muuttujan kautta on näkyvissä toisen kautta, ilman että käytetään `&`-operaattoria. `clone $a` luo uuden olioinstanssin, jossa on matala kopio alkuperäisen olion ominaisuuksista. Jos ominaisuus on itsessään olio, kloonatun olion ominaisuus osoittaa edelleen samaan sisäkkäiseen olioon, ellet erikseen kloonaa sitä. `__clone()`-metodia kutsutaan uudessa oliossa matalan kopion jälkeen, ja se on oikea paikka tunnusten nollaamiseen, resurssien irrottamiseen tai sisäkkäisten olioiden syvän kopion tekemiseen. Syvä kopio tarkoittaa sisäkkäisten muutettavissa olevien (mutable) olioiden rekursiivista kopiointia, joita ei tulisi jakaa.

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;
Kokeile vastata tähän kysymykseen tekoälyvalmentajan kanssa

Senior-tason kysymykset

13Miten vianmäärittäisit tietokantayhteyksien ehtymisen (connection exhaustion) ja suunnittelisit yhteyshallinnan PHP-FPM:lle ja pitkäkestoisille työsäikeille?

Vianmääritykseen vertaisin tietokannan yhteysrajoituksia ja nykyisiä istuntoja PHP:n kokonaisrinnakkaisuuteen: jokainen aktiivinen PHP-FPM-lapsiprosessi voi pitää yllä omaa tietokantayhteyttään, ja useat sovellusaltaat (app pools), isännät, cron-tehtävät, jonotyöntekijät (queue workers), migraatiot ja käyttöönotot lisäävät kysyntää. Tarkistaisin tietokannan istuntonäkymät, yhteyksien tilat, `idle-in-transaction`-istunnot, hitaat kyselyt, lukitusodottamiset (lock waits), virhelokit, poolin metatiedot ja viimeaikaiset liikenne- tai työntekijämäärän muutokset. PHP-FPM:n osalta pitäisin `max_children`-arvon ja isäntäkohtaisen rinnakkaisuuden tietokantabudjetin puitteissa, käyttäisin lyhyitä pyyntökohtaisia transaktioita/yhteyksiä, tekisin `commit`- tai `roll back` -toimintoja nopeasti ja välttäisin yhteyksien pitämistä auki hitaiden ulkoisten kutsujen aikana. Pysyvät PDO-yhteydet voivat vähentää yhteydenmuodostuksen yleiskustannuksia, mutta ne voivat sitoa useita joutokäynnillä olevia yhteyksiä FPM-prosessia kohden ja säilyttää istuntotilan, joten ne vaativat varovaisuutta. Yhteyspooleri tai välityspalvelin (proxy), kuten PgBouncer, voi multipleksoida (multipleksata) useita PHP-asiakkaita pienemmäksi määräksi tietokantapalvelimen yhteyksiä, kun sovellus on yhteensopiva. Pitkäkestoiset työsäikeet tarvitsevat eksplisiittistä yhteyden elinkaaren hallintaa: kunnontarkistukset, uudelleenyhteyden muodostaminen joutokäyntiaikakatkaisujen/uudelleenkäynnistysten/vikatilanteiden/forkkausten jälkeen, istuntotilan nollaaminen ja vain turvallisten tai idempotenttisten töiden uudelleenyrittäminen.

Example:
- 6 application servers
- 40 PHP-FPM max_children per server
- 2 separate FPM pools that can hit the database

Potential concurrent PHP request DB connections = 6 * 40 * 2 = 480

If PostgreSQL max_connections is 300 and queues/cron/admin tools also connect,
connection exhaustion is expected during traffic spikes.
Kokeile vastata tähän kysymykseen tekoälyvalmentajan kanssa

14Miten käsittelisit lukukopioita (read replicas), luku-/kirjoitusliikenteen jakamista (read/write splitting), replikointiviivettä (replication lag) ja ensisijaisen tietokannan vikasietotilannetta (primary database failover) PHP:ssä?

Lukukopioiden ja vikasietotilanteiden (failover) käsittely PHP:ssä edellyttää luku- ja kirjoitusliikenteen jakamista, replikointiviiveen lieventämistä ja yhteyshäiriöiden hallintaa vikasietotilanteen aikana. Sovellustasolla tietokannan konfiguraatio tai väliohjelmisto (middleware) ohjaa lukukyselyt (`SELECT`) lukukopioille ja kirjoituskyselyt (`INSERT`, `UPDATE`, `DELETE`) ensisijaiseen instanssiin (vaihtoehtoisesti hallitaan tietokantavälityspalvelimien, kuten ProxySQL:n tai AWS RDS Proxyn, kautta). Koska replikointi on asynkronista, replikointiviive voi aiheuttaa vanhentuneita lukuja. Varmistaakseen 'read-your-writes' (lue omat kirjoituksesi) -konsistenssin, sovelluksen tulisi ohjata aktiivisen transaktion sisäiset lukukyselyt ensisijaiseen tietokantaan ja tilapäisesti ohjata myöhemmät lukukyselyt ensisijaiseen tietokantaan tietyn käyttäjäistunnon tai pyynnön elinkaaren ajaksi välittömästi kirjoituksen jälkeen. Ensisijaisen tietokannan vikasietotilanteessa nykyaikaiset tietokantaklusterit ylentävät replikan ensisijaiseksi ja päivittävät DNS-/välityspalvelimen päätepisteet. PHP:ssä yhteyskerroksen on siepattava yhteyskatkokset ja vain luku -virheet, pudotettava vanhentuneet PDO-socketit ja sovellettava hallittuja uudelleenyrityksiä eksponentiaalisen viiveen (exponential backoff) kanssa.

<?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',
    ],
];
Kokeile vastata tähän kysymykseen tekoälyvalmentajan kanssa

15Miten tietokantamigraatiot ja skeemamuutokset otetaan turvallisesti käyttöön nollalla tai minimaalisella käyttökatkolla PHP-sovelluksissa?

Nollakatkon tietokantamigraatioiden käyttöönotto PHP-sovelluksissa edellyttää skeeman kehityksen erottamista sovelluskoodin käyttöönotoista käyttämällä Expand and Contract (Rinnakkaisajo) -mallia yhdessä estämättömien online-DDL (Data Definition Language) -käytäntöjen kanssa. Laajennusvaiheessa (Expand phase) otetaan käyttöön taaksepäin yhteensopivia skeemamuutoksia (esim. nollattavissa olevien sarakkeiden tai uusien taulukoiden lisääminen tai samanaikaiset indeksit käyttäen työkaluja kuten `pt-online-schema-change`, `gh-ost` tai natiivi online-DDL MySQL/PostgreSQL:ssä). Sovellus otetaan sitten käyttöön tukemaan kaksoiskirjoitusta (kirjoitus sekä vanhoihin että uusiin skeemarakenteisiin) samalla kun luetaan kummastakin. Historialliset tiedot täytetään sitten takaisin asynkronisesti rajoitetuissa taustaerissä, jotta vältetään taulukoiden lukitseminen tai replikaatioviiveen aiheuttaminen. Supistusvaiheessa (Contract phase), kun tietojen eheys on validoitu ja lukeminen on siirretty kokonaan uuteen rakenteeseen, koodin käyttöönotto poistaa viittaukset vanhaan skeemaan. Lopuksi siivousmigraatio poistaa vanhentuneet sarakkeet tai taulukot. Turvallinen palautus edellyttää, että jokainen välitila skeemasta pysyy täysin yhteensopivana sekä edellisten että tulevien sovellusversioiden kanssa vaiheittaisten julkaisujen aikana.

<?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
Kokeile vastata tähän kysymykseen tekoälyvalmentajan kanssa