12: Geavanceerd

Phlo blijft opzettelijk modulair. Je kunt een app klein houden en alleen de resources activeren die het nodig heeft, of meerdere bronpaden en resourcegroepen combineren.

12.1: App-code en runtime-resources

App-specifieke code hoort in de app zelf. Plaats het niet in /opt/phlo/resources/.

/opt/phlo/resources/ is de Phlo runtime catalogus: framework-brede bronnen die mogelijk gedeeld worden tussen meerdere apps en opzettelijk naast de runtime worden onderhouden. Alleen generieke, stabiele code hoort daar thuis.

Als je code tussen apps wilt delen, maak dan eerst een expliciete gedeelde module of app-bibliotheekpad met een duidelijke eigenaar. Promoot code alleen naar de Phlo runtime catalogus wanneer het echt frameworkfunctionaliteit is.

Een runtime-resource kan een object, functie, stijl of script bieden. Metadata aan de bovenkant van het bestand helpt het Phlo Control Center en de handleiding:

@ summary: Send app notifications
@ package: notifications
@ frontend: false
@ backend: true

method send($message){
	return HTTP(%creds->notify->url, POST: ['message' => $message])
}

12.2: Meerdere brondirectory's

Houd de standaard eenvoudig: app-bron in het app-pad. Voeg alleen extra paden toe wanneer een codebase echt gedeeld moet worden.

Padkeuzes moeten voorspelbaar blijven:

12.3: Integreren met bestaande PHP

Gebruik Phlo naast bestaande PHP door de runtime te laden en de Phlo entrypoint alleen verantwoordelijk te maken voor de routes die de app beheert. Bestaande statische bestanden blijven rechtstreeks door de webserver worden bediend.

<?php
require('/opt/phlo/phlo.php');
phlo_app (
	id: 'Legacy',
	host: 'dev.legacy.test',
	build: true,
	debug: true,
	app: '/var/www/legacy/',
);

12.4: Beveiliging en bezoekers

Voor openbare sites is de gebruikelijke basislijn:

{
    "resources": [
        "cookies",
        "security/security",
        "security/token",
        "session",
        "useragent",
        "visitors",
        "phlo.async",
        "DOM/form"
    ]
}

Voor lokale ontwikkeling kun je tracking uitsluiten:

{
    "exclude": [
        "visitors",
        "useragent"
    ]
}

De visitors resource volgt de betrokkenheid, niet alleen het aantal hits. Een klein heartbeat-script accumuleert actieve tijd terwijl het tabblad daadwerkelijk zichtbaar is (een monotone klok, gepauzeerd op hidden, gewist met een keepalive-verzoek bij unload) in active_seconds, en registreert een per-pagina rij in een visitor_pages tabel, zodat je de tijd per pagina kunt zien, niet alleen de landings-URL. Het is standaard bewust van toestemming (zie de cookiewall hieronder): met toestemming slaat het het IP-adres en de volledige browser, OS en apparaat op; zonder toestemming wordt de bezoeker geïdentificeerd door een dagelijkse hash zonder IP en een gehashte browserstring. Bots worden overgeslagen.

12.4.1 CSP-modi

security/security stelt de basisreactiekoppen in (Referrer-Policy, nosniff, frame- en cross-origin-beleid) en een Content-Security-Policy. Kies het beleid dat overeenkomt met het oppervlak van de app en roep de bijbehorende methode aan:

Modus Beleid Gebruik voor
%security->strict() nonce-gebaseerd: alleen 'nonce-...' scripts en stijlen, plus Cache-Control: no-store apps met een XSS-oppervlak (gebruikersinhoud, formulieren)
%security->basic() 'self' scripts, inline stijlen toegestaan statische of vertrouwde-inhoud sites
%security->marketing() zoals basic, plus img-src https: openbare pagina's die externe afbeeldingen ophalen
%security->api() default-src 'none' JSON-only eindpunten

Onder debug versoepelen basic en marketing script-src naar 'unsafe-inline' zodat de inline debugconsole kan draaien; strict gebruikt altijd een nonce, zodat een app met een XSS-oppervlak vergrendeld blijft, zelfs met debug aan. In strict modus, render de per-verzoek nonce op je <script>/<style> tags via %app->nonce.

12.5: Cookiewall: GDPR-toestemming

DOM/cookiewall is een ingebouwde, subtiele toestemmingsbanner. Activeer het in 3 stappen:

1. Resource in data/app.json:

{ "resources": [..., "DOM/cookiewall"] }

2. Banner in je lay-out:

view layout:
<body>
	{{ %cookiewall->banner }}
	<main>...</main>
</body>

De banner verschijnt alleen wanneer de bezoeker nog geen keuze heeft gemaakt. Twee knoppen: "Alleen essentieel" en "Accepteren". De keuze wordt opgeslagen in een cookie cookieChoice ('essential' of 'all'), geldig voor 1 jaar.

3. Tracking volgt de keuze automatisch: de ingebouwde visitors resource heeft geen beveiliging en geen analytics-script nodig. De heartbeat bevat de keuze, en de server slaat IP, browser, OS en apparaat alleen op na "Accepteren"; zonder toestemming telt de bezoeker nog steeds mee, anoniem (een dagelijkse hash, geen IP).

Methode Retourneert
%cookiewall->hasChosen() true zodra de bezoeker iets heeft gekozen
%cookiewall->canTrack true alleen voor de 'all' keuze
%cookiewall->canAnalytics Alias van canTrack, semantisch nuttig voor een analytics-brug
%cookiewall->choice 'essential' / 'all' / null

Talen: Engels is standaard, en het vertaalt automatisch zodra het taalsysteem (en()) is geladen, zodat een meertalige app geen extra configuratie nodig heeft. Overschrijf prop labels om de teksten voor een vaste taal in te stellen, of prop translate om vertaling in of uit te schakelen.

12.5.1: Captcha

security/captcha is een zelf-contained slider-puzzle captcha: geen externe service en geen scripts van derden. De server kiest een geheime gap-positie, rendert de achtergrond en het losse stuk met GD, en de bezoeker sleept het stuk op zijn plaats; de gap-positie verlaat nooit de server. Het vereist de GD-extensie.

Render de widget binnen je formulier met %captcha->widget:

<form.async method=post action="/signup">
	...
	{{ %captcha->widget }}
	<button>Sign up</button>
</form>

De widget bevat twee verborgen velden, captcha_x en captcha_t, die door het gebundelde script worden ingevuld met de droppositie en de drag-telemetrie. Bij indienen, lees die twee velden en controleer de actie:

if (!captcha::verify($x, $telemetry)){
	return apply(errors: ['captcha' => 'Drag the slider to continue'])
}
captcha::consume()

verify($x, $telemetry) controleert de droppositie tegen het geheim van de server en weigert niet-menselijke slepen (te snel, te weinig monsters, geen padvariatie). Het wist de uitdaging niet, dus roep consume() pas aan na een succesvolle controle.

12.5.2: Social login

security/social voegt "Aanmelden met Google" (en Microsoft en Apple) toe bovenop security/OAuth2: het bouwt de autorisatie-URL en verandert de callback-code in een geverifieerd profiel, zodat jouw app alleen de gebruikerskant beheert. Het leest de client_id en client_secret van elke provider uit een [google] (of [microsoft], [apple]) sectie in data/creds.ini; een provider zonder inloggegevens is simpelweg niet beschikbaar.

Een aanmelding bestaat uit twee routes. De eerste stuurt de bezoeker naar de provider met een eenmalige state die in de sessie wordt bewaard:

route GET auth google {
	%session->social_state = $state = bin2hex(random_bytes(16))
	return location(social::authUrl('google', $state))
}

De tweede is de redirect URI die je hebt geregistreerd bij de provider, /auth/google/callback. Controleer de state, wissel de code om, en je krijgt een genormaliseerd, handtekening-geverifieerd profiel terug:

route GET auth google callback {
	if (!hash_equals((string)%session->social_state, (string)%req->query['state'])) return location('/login')
	$profile = social::profile('google', (string)%req->query['code'])
	if (!$profile || !$profile['verified']) return location('/login')
	// $profile: provider, uid, email, name, verified
}

profile() verifieert de handtekening van het id_token tegen de JWKS van de provider voordat enige claim wordt vertrouwd, en controleert de doelgroep, vervaldatum en uitgever. Sleutel het account op provider plus uid (het stabiele onderwerp), en beschouw de e-mail als een claim: neem een bestaand lokaal account alleen op basis van e-mail aan wanneer de provider daadwerkelijk het eigendom ervan heeft bewezen. De resource heeft geen mening over gebruikers, sessies of routes; dat blijft aan jou.

12.6: Worker modus

Standaard draait Phlo per verzoek: het PHP-proces start, verwerkt het verzoek en eindigt. Met thread: true in phlo_app(...) blijft de runtime in het geheugen tussen verzoeken, bedoeld voor FrankenPHP, ReactPHP of RoadRunner.

De prestatiewinst is groot (geen opstart per verzoek), maar er gelden drie regels:

1. Geen die() of exit() in het HTTP-pad. Beide beëindigen de hele worker, niet alleen het huidige verzoek. Gebruik return of laat een beëindigende aanroep (view(), apply(), location()) de respons verzenden.

2. Geen verzoekstatus in statische eigenschappen. Statics overleven tussen verzoeken. Gegevens van verzoek A lekken in verzoek B. Statics zijn alleen veilig voor klassenstructuur of berekende metadata die identiek is voor alle verzoeken, niet voor sessie, gebruiker, payload, tijd of DB-status.

3. Markeer langlevende objecten met $objPers = true. Standaard wist Phlo zijn instantiekart tussen verzoeken. Voor objecten die je expliciet wilt hergebruiken (DB-verbinding, voorbereide statements), stel $this->objPers = true in zodat de opruiming ze met rust laat.

De databaseverbinding is het gebruikelijke voorbeeld, en deze is standaard tijdelijk: %MySQL houdt zijn PDO niet vast tussen verzoeken, dus een inactieve verbinding die de database heeft verbroken (de laag-verkeer "server is weg") kan nooit opnieuw worden gebruikt als deze verouderd is. Kies voor één persistente verbinding met prop %MySQL.objPers = true vanuit app.phlo; query() maakt opnieuw verbinding en probeert het eenmaal bij een verbroken verbinding, zodat die keuze veilig blijft.

Combineren met build: true is niet toegestaan: build schrijft bestanden tussen verzoeken, en in een worker is dat een raceconditie. Phlo gooit een runtime-fout als je beide inschakelt.

12.7: Hulpmiddelen aanpassen zonder te forkeren

Soms wil je dat een gedeelde resource net iets anders werkt in één app, zonder die resource te kopiëren of te veranderen. Vanuit elk .phlo-bestand kun je een node in een andere klasse injecteren of overschrijven door de node te benoemen als %<class>.<node>:

static %visitors.table = 'control.visitors'
prop %visitors.db = 'control'
method %model.greet => 'hi'

De eerste regel overschrijft de static $table van het visitors model; de tweede voegt een db prop toe aan visitors; de derde voegt een greet method toe aan model. Tijdens de build verwijdert de transpiler de %<class>. prefix en schrijft de node in <class>: een bestaande node met die naam wordt overschreven, er wordt een nieuwe toegevoegd. De doelklasse moet deel uitmaken van de build (de resource moet geladen zijn), anders wordt de modifier stilletjes genegeerd. Houd het nodetype identiek aan wat je vervangt (static met static, prop met prop): de hele node wordt verwisseld.

Een praktisch voorbeeld: laat het gedeelde visitors model schrijven naar een centrale analytics database, terwijl alle andere queries in de app op de eigen verbinding van de app blijven:

static %visitors.table = 'control.visitors'

Dit houdt de gedeelde resource agnostisch terwijl elke app zijn eigen interpretatie geeft.

12.8: Bestand metadata: de complete @ referentie

Elke .phlo-bestand kan worden geopend met @ key: value-regels. Elke sleutel wordt opgeslagen als bestandsmetadata; deze hebben een engine- of tooling-betekenis:

Sleutel Effect
@ class: Overschrijf de PHP-klassenaam
@ extends: PHP-erfelijkheid (standaard: obj)
@ implements: PHP-interfaces, gescheiden door komma's
@ use: PHP use-verklaring (Full\Name as Alias)
@ namespace: PHP-namespace
@ type: class (standaard), abstract class, interface of trait
@ summary: Een-regelige beschrijving, weergegeven in de handleiding, reflectie en het Phlo Control Center
@ package: Groepsnaam voor tooling
@ frontend: / @ backend: Markeert een resource als alleen frontend- of backend
@ requires: Afhankelijkheden, opgelost wanneer de resource is ingeschakeld; name? is optioneel, php-ext: en creds: vermeldingen zijn informatief
@ provides: / @ binds: Frontend-API's aangeboden / selectors gekoppeld; voedt reflect::selectorGraph
@ tags: Vrije labels, weergegeven in reflectie-indexen
@ advice: Ontwikkelaarsrichtlijnen, weergegeven in reflect::objectIndex

12.9: Best practices

We gebruiken essentiële cookies om deze site te laten werken. Met uw toestemming gebruiken we ook analytics om de site te verbeteren.