Când Elite Business Club Romania a avut nevoie de o platformă de învățare online, fiecare plugin WordPress LMS existent pe piață a ratat ținta într-un fel sau altul. Așa că l-am construit pe al nostru, un plugin WordPress de nivel producție care gestionează cursuri, înscrieri, plăți, sesiuni live Zoom, corectare teme, tutoriat 1:1, diplome și o postură de securitate cuprinzătoare. Acesta este povestea ingineriei din spatele APEX Digital LMS.
De ce nu am folosit LearnDash sau Tutor LMS
Piața pluginurilor WordPress LMS este matură. LearnDash, Tutor LMS, LifterLMS și Sensei oferă gestionarea cursurilor din cutie. Atunci de ce să construim de la zero?
Răspunsul nu este filozofic, ci practic. Cerințele clientului nostru se suprapuneau parțial cu ce ofereau pluginurile existente, dar combinația specifică făcea orice soluție gata disponibilă nepotrivită:
- Plăți native WooCommerce: nu un coș separat sau checkout exclusiv Stripe, ci fluxul complet WooCommerce cu cupoane, facturare, gestionarea comenzilor și compatibilitate HPOS
- Integrare Bricks Builder: întregul frontend este construit în Bricks, astfel că paginile de curs au nevoie de funcții de template și query loop personalizate Bricks, nu template-uri randate prin shortcode
- Hosting video exclusiv Vimeo cu protecție anti-derulare și progres verificat server-side, nu un oEmbed generic
- Sesiuni Zoom live integrate în browser cu acces bazat pe rol, gazdă sau participant, determinat de statusul înscrierii
- Sesiuni de tutoriat 1:1 ca tip separat de curs, cu programare, schimb de fișiere și email-uri de reamintire
- Prețuri pe două niveluri: același curs vândut ca standard sau standard+tutoriat la prețuri diferite, ambele prin WooCommerce
- Limba română pe primul loc, cu reguli tipografice specifice: exclusiv majusculă la început de propoziție, fără culori albastre
Oricare cerință individuală ar fi putut fi rezolvată cu un plugin existent plus cod personalizat. Toate la un loc însemnau că am petrece mai mult timp luptând cu arhitectura pluginului decât construind funcționalități. Un plugin personalizat ne-a oferit control total asupra modelului de date, punctelor de integrare și experienței utilizatorului.
Arhitectura: un service layer în interiorul WordPress
Pluginurile WordPress tind spre un stil „hooks și funcții", cod procedural răspândit în callback-uri de acțiuni. Funcționează pentru pluginuri mici, dar un LMS are destule subsisteme care interacționează încât aveam nevoie de structură.
Am ales un pattern de service layer. Fiecare preocupare de domeniu trăiește în propria clasă de serviciu:
Enrollment_Service: creare, interogare și gestionare a înscrierilor la cursuriProgress_Service: urmărirea progresului per modul și per cursHomework_Service: trimitere, corectare, gestionare fișiereAccess_Service: determinarea cine poate vedea ceTutoring_Service: programarea și ciclul de viață al sesiunilorQuiz_Service: tentative, punctare, măsuri anti-fraudăDiploma_Service: generare, URL-uri securizate, randarea template-urilorRating_Service: votarea conținutului și agregarea rezultatelor
Fiecare serviciu este înregistrat ca singleton pe clasa principală a pluginului și
accesat prin
apex_lms()->enrollment(), apex_lms()->progress() etc.
Serviciile comunică între ele prin apeluri de metode, nu prin hook-uri. Hook-urile sunt
rezervate pentru punctele de integrare externă:
apex_lms_user_enrolled,
apex_lms_course_completed, apex_lms_homework_submitted,
declanșate după ce operațiunea internă reușește.
Această separare are un beneficiu practic: când WooCommerce declanșează
woocommerce_order_status_completed, handler-ul nostru de comenzi apelează
direct Enrollment_Service::enroll(). Serviciul de înscriere gestionează
validarea, verificările de duplicat și tranzițiile de status într-un singur loc,
indiferent dacă înscrierea vine dintr-o comandă WooCommerce, un formular admin sau
un apel REST API.
Motorul de înscrieri: webhook-uri concurente în siguranță
Înscrierea pare simplă: utilizatorul cumpără cursul, utilizatorul primește acces. În practică, gateway-urile de plată trimit webhook-uri concurent, WooCommerce declanșează multiple tranziții de status per comandă, iar personalul admin înscrie manual studenți în același timp. Patternul naiv „verifică dacă e înscris, apoi inserează" este un clasic race condition TOCTOU.
Abordarea noastră: inserează mai întâi, gestionează conflictele. Tabelul
de înscrieri are o
UNIQUE KEY (user_id, course_id). În loc să verificăm înainte de inserare,
încercăm inserarea și lăsăm baza de date să impună unicitatea. La duplicate key,
preluăm înscrierea existentă: dacă este activă sau finalizată, o returnăm idempotent;
dacă este suspendată sau expirată, o reactivăm cu noile date de plată.
Gestionarea rambursărilor a fost mai complicată decât se anticipa. Patternurile LIKE
din WooCommerce în căutările comandă-înscriere tratau caracterele underscore literale
ca wildcard-uri, potrivind silențios zero rânduri. Rambursările nu suspendau de fapt
nimic. Am descoperit acest lucru în cadrul unui audit de securitate sistematic (mai
multe despre asta mai târziu) și l-am rezolvat cu apeluri corecte la
$wpdb->esc_like().
Nu te baza niciodată pe patternuri „verifică-apoi-acționează" în codul unui plugin WordPress. Gateway-urile de plată, job-urile cron și acțiunile admin rulează concurent. Folosește constrângerile bazei de date ca sursă a adevărului.
Urmărirea progresului: cinci tipuri de module, un sistem unificat
Un curs în APEX LMS poate conține cinci tipuri de module, fiecare cu criterii diferite de finalizare:
- Module video: finalizare la 90% din timpul de vizionare (configurabil), verificat server-side față de timpul efectiv scurs pentru a preveni trișarea prin derulare rapidă
- Module text: buton manual „marchează ca finalizat"
- Module quiz: automat la obținerea punctajului de trecere
- Module sesiuni live: marcate ca finalizate după fereastra sesiunii sau prin quiz-ul asociat
- Module examen: quiz cronometrat cu dată programată, supraveghere opțională prin Zoom
Progresul la nivel de curs reprezintă procentajul modulelor finalizate. Când atinge
100%, sistemul declanșează apex_lms_course_completed, care inițiază
generarea diplomei, un email de felicitare și actualizarea statusului de înscriere,
toate înfășurate atomic într-o tranzacție de bază de date cu o gardă
UPDATE ... WHERE completed_at IS NULL
pentru a preveni evenimentele de finalizare duplicate din cereri concurente.
Teme și evaluare: adevărul pe server
Două principii au ghidat implementarea quiz-urilor și temelor:
- Nu trimite niciodată răspunsurile corecte în browser. Întrebările de quiz sunt livrate fără marcajele de răspuns. Când studentul trimite, serverul punctează tentativa. Nu este securitate de formă. Este singura modalitate de a face evaluările online semnificative.
- Tokenizează tot accesul la fișiere. Fișierele temelor sunt stocate
într-un director protejat cu reguli de refuz
.htaccess. Descărcările trec printr-un endpoint AJAX verificat cu nonce care verifică proprietatea (student, tutore sau admin). URL-urile directe nu funcționează.
Fluxul de lucru pentru teme este simplu: studentul încarcă un fișier cu note opționale, trimiterea primește statusul „în așteptare", un admin o revizuiește și fie o aprobă, solicită revizuire, fie o marchează ca revizuită. Fiecare schimbare de status declanșează un email. În cursurile cu acces secvențial, o temă în așteptare sau respinsă blochează progresia la modulul următor: studentul pur și simplu nu poate continua până când adminul aprobă.
Tentativele de quiz folosesc un mecanism atomic de pornire:
INSERT ... SELECT FROM DUAL WHERE NOT EXISTS
(... AND completed_at IS NULL). Aceasta previne bug-ul subtil în care două click-uri
concurente pe „Începe quiz-ul" creează două tentative incomplete.
Sesiuni live și Zoom: 13 iterații până la soluția corectă
Integrarea Zoom are cea mai agitată istorie de dezvoltare din toate funcționalitățile acestui plugin. Jurnalul nostru de modificări spune povestea în 13 versiuni, de la v3.0.5 la v3.8.0.
Tentativa 1: Client View SDK. „Client View" din Zoom Meeting SDK preia
tot viewport-ul browserului. A funcționat, dar prepareWebSDK() injecta
CSS-ul Zoom la încărcarea paginii, stricând tipografia, butoanele și layout-ul pe
întreaga pagină. Am amânat încărcarea SDK-ului până la momentul conectării. Mai bine,
dar utilizatorul pierdea tot contextul paginii.
Tentativa 2: Component View SDK într-un dialog. Component View se
randează într-un element container, așa că l-am pus într-un <dialog> HTML
nativ. Aceasta a introdus o cascadă de probleme: „top layer"-ul dialogului bloca
elementele MUI portal (dropdown-urile și meniurile Zoom); vizualizarea galerie a SDK-ului
se extindea la dimensiunile rezoluției ecranului în timpul partajării ecranului,
împingând barele de instrumente în afara ecranului; iar un șir de localitate românesc
(ro-RO) bloca silențios SDK-ul deoarece acesta suportă doar 12 localități.
Tentativa 3: Component View într-un div fix. Am înlocuit
<dialog> cu un overlay
position: fixed. Portalurile MUI au funcționat din nou, dar
dimensiunile maxime hard ale SDK-ului (1440x810px) intrau în conflict cu CSS-ul nostru
care încerca să umple viewport-ul. Am adăugat ajutoare computeViewSizes()
și listeneri de redimensionare. A funcționat în mare parte, până când nu a funcționat,
în cazuri limită pe care le tot descopeream.
Soluția finală: Client View într-un tab nou. Am simplificat totul.
Butonul de conectare este acum un simplu link
<a target="_blank">. Deschide o pagină HTML standalone servită de
WordPress (?apex_zoom_meeting=1) care încarcă Zoom Client View SDK din
CDN, generează semnătura JWT server-side (fără round-trip AJAX) și se conectează
automat. La ieșire, tab-ul se închide sau afișează un link „înapoi la curs".
Total CSS eliminat: ~128 de linii de reguli pentru dialog. Total JavaScript eliminat:
întregul fișier
zoom-meeting.js. Total bug-uri rezolvate: toate.
Nu integra framework-uri UI terțe complexe (React + MUI) în contextul paginii WordPress. Conflictele CSS și DOM sunt nesfârșite. Oferă SDK-ului propria pagină curată și comunică prin URL-uri simple.
Sistemul de tutoriat: sesiuni 1:1 și variante pe niveluri
APEX LMS suportă două tipuri de cursuri. Cursurile standard sunt bazate pe module: studentul progresează prin videoclipuri, texte, quiz-uri și sesiuni live. Cursurile de tutoriat sunt bazate pe sesiuni: un admin programează întâlniri 1:1 între un tutore și un student.
În versiunea 3.12, am adăugat o a treia opțiune: variante pe niveluri. Un curs standard poate oferi opțional un nivel de tutoriat 1:1 la un preț separat. Sistemul creează automat două produse WooCommerce per curs. Tabelul de înscrieri stochează ce nivel a fost achiziționat. Crearea sesiunilor de tutoriat este condiționată de nivelul de înscriere. Doar studenții care au cumpărat nivelul 1:1 pot programa sesiuni.
Ciclul de viață al sesiunii este simplu, dar are automatizări semnificative:
- Adminul creează sesiunea: studentul primește email de confirmare
- Cu 24 de ore înainte: atât studentul cât și tutorele primesc email-uri de reamintire
- La ora sesiunii: apare badge-ul „Live acum", butonul Zoom de conectare se activează
- Fereastra de 2 ore trece: sesiunea se mută în „sesiuni trecute"
- Adminul anulează: studentul primește email de anulare
Studenții pot adăuga orice sesiune în calendarul lor (Google, Outlook, Yahoo sau
Apple/ICS) printr-un buton dropdown. Fișierele ICS sunt generate server-side cu
gestionare corectă a fusului orar, o lecție învățată pe calea grea după ce
strtotime() a tratat orele locale românești ca UTC, decalând totul cu 2 ore.
Diplome: generate automat, optimizate pentru tipărire, rezistente la fraudă
Când un student finalizează un curs, sistemul generează o diplomă de participare cu
un URL hex unic de 32 de caractere (/diploma/{hash}/). Nu este necesară
autentificarea pentru a o vizualiza. Hash-ul este tokenul de acces. Aceasta înseamnă
că studenții o pot distribui pe LinkedIn, adăuga în CV sau tipări direct.
Oferim două template-uri: un design minimalist modern și un design clasic formal cu
borduri aurii, tipografie serif Playfair Display și o sigiliu ornamental. Ambele sunt
pagini HTML optimizate pentru tipărire A4 landscape cu reguli @page,
print-color-adjust: exact și dimensiuni în mm.
Sigiliul a trecut prin cinci iterații: starburst SVG desenat manual, starburst calculat cu 24 de puncte, design medalion/monedă și în final o imagine de panglică fotorealistă. Calculele SVG corecte pentru starburst (raza exterioară 50, raza interioară 42, spațiate uniform la intervale de 7,5 grade) au fost un memento că detaliile vizuale contează la fel de mult ca logica backend.
Prevenirea dublei emiteri folosește un pattern atomic
UPDATE ... WHERE diploma_issued = 0.
Doar cererea care schimbă flag-ul de la 0 la 1 declanșează evenimentul de diplomă.
Auditul de securitate cu 86 de probleme
Între versiunile 2.27 și 3.0, am realizat un audit complet de securitate și calitate în cinci faze. Cifrele:
- 13 probleme critice: race conditions în înscrieri, membership și emiterea diplomelor; vulnerabilități IDOR în revizuirea temelor; validare lipsă a încărcărilor de fișiere; vectori de injecție SQL în patternurile LIKE
- 22 probleme ridicate: lacune de control al accesului în endpoint-urile REST și handler-ele AJAX; output ne-escaped; verificări de capability lipsă; securitatea fișierelor (descărcări tokenizate)
- 31 probleme medii: validarea argumentelor REST, limitarea ratei, expunerea datelor în script-uri localizate, inconsistențe de fus orar
- 20 probleme de performanță: interogări N+1, cache-uri lipsă, interogări nelimitate, apeluri DB redundante
Remedierile cele mai impactante au vizat securizarea descărcărilor de fișiere temă.
Anterior, fișierele erau accesibile prin URL-ul direct: oricine ghicea sau intercepta
calea putea descărca tema altui student. Am mutat fișierele într-un director protejat,
am adăugat reguli de refuz .htaccess și am direcționat toate descărcările
printr-un endpoint verificat cu nonce care verifică proprietatea înainte de a transmite
fișierul cu readfile().
O altă remediere critică: metoda can_access_course(), un predicat
read-only care ar fi trebuit să returneze adevărat/fals, apela
enroll() ca efect secundar. Aceasta însemna că verificarea accesului la
curs putea crea înscrieri, ducând la înscrieri fantomă după rambursări.
Performanță: de la interogări N+1 la operații în lot
Faza de performanță a auditului s-a concentrat pe eliminarea interogărilor inutile
în baza de date. Cel mai mare câștig:
get_next_module(), apelat la fiecare încărcare de dashboard, pagină
de curs și cerere AJAX. Pentru un curs cu 10 module, executa 11 interogări (1 pentru
lista de module + 10 căutări individuale de progres). L-am înlocuit cu o singură
interogare LEFT JOIN
cu LIMIT 1.
Alte optimizări:
- Recalcularea progresului: am unit 3 interogări separate (număr finalizat, ultimul modul, sumă timp) într-o singură interogare cu agregare condiționată
- Statistici agregate Bricks: o singură interogare
SUM()a înlocuit N căutări per-înscriere în funcțiile de template - Încărcare fișiere în lot: fișierele sesiunilor de tutoriat încărcate cu o interogare
INîn loc de N apeluri per-sesiune - Cache-uri serviciu quiz: cache-urile per-cerere au redus interogările paginii de quiz de la 8-9 la 3
- Prepararea metadatelor post:
update_meta_cache('post', $course_ids)pregătește meta pentru toate cursurile înscrise într-o singură interogare
Am adăugat și gărzi practice: LIMIT 500 pe interogările nelimitate,
no_found_rows => true pe interogările care nu necesită numărarea
paginilor, și gărzi de programare cron bazate pe transient care omit
wp_next_scheduled()
pentru 99,9% din încărcările de pagini.
Sistemul de design: constrângerile ca funcționalități
Două constrângeri definesc identitatea vizuală a APEX Digital LMS:
- Fără culori albastre. Nicăieri. Paleta este zinc grays, verde accent
(
#2e981a) și alb. Fără excepții: nu în admin, nu în email-uri, nu în badge-urile de status. Când am găsit trei valori hex albastre în CSS-ul admin în timpul auditului, le-am înlocuit imediat. - Exclusiv majusculă la început de propoziție. Fără title case în titluri, butoane, etichete sau mesaje de eroare. „Trimite tema" nu „Trimite Tema". Aceasta se aplică atât în română cât și în engleză. Este o regulă mică cu un efect disproporționat asupra consistenței vizuale.
Token-urile de design sunt definite ca proprietăți CSS personalizate într-un singur fișier:
apex-lms-design-tokens.css. Spațierea urmează o scară de bază de 4px.
Tipografia folosește Inter pentru textul de corp și Manrope pentru titluri. Fiecare
culoare, valoare de spațiere, umbră și rază de bordură are un token. CSS-ul admin are
propriul set de token-uri pentru stilizare consistentă pe paginile admin WordPress.
Am consolidat trei sisteme separate de butoane într-un singur sistem canonic
.btn,
am eliminat 75 de referințe la un spațiu de nume de variabile legacy
--apex-* și
am standardizat toate culorile template-urilor de email la paleta zinc. Nu sunt
modificări spectaculoase, dar reprezintă diferența dintre un codebase care se simte
ca un produs și unul care se simte ca o colecție de funcționalități.
Protecția conținutului: descurajare pragmatică
Un LMS care vinde cursuri cu plată are nevoie de o formă de protecție a conținutului. Suntem realiști în privința a ceea ce poate realiza protecția client-side. Un utilizator hotărât cu DevTools poate întotdeauna extrage conținut. Dar putem face copierea ocazională suficient de incomodă încât majoritatea utilizatorilor nu se vor obosi.
Sistemul de protecție oferă trei straturi, fiecare activabil independent:
- Anti-copiere pe paginile LMS: dezactivează selecția textului, click-dreapta și scurtăturile de copiere pe paginile de curs și modul
- Protecție la nivel de site: extinde aceleași protecții la întregul frontend, cu excepții pentru formulare și elemente interactive
- Blocare DevTools: interceptează F12, Ctrl+Shift+I/J/C și scurtăturile Ctrl+U. Administratorii sunt automat exceptați.
Implementarea normalizează cu atenție evenimentele DOM. Un ajutor toElement()
convertește nodurile Text și Comment la elementul lor părinte Element înainte de a
apela matches()/closest(),
prevenind TypeError-urile la interacțiunile cu text brut.
Câmpurile de formular, bara de admin WordPress și elementele interactive sunt pe lista
albă astfel încât site-ul rămâne complet funcțional.
Email-uri tranzacționale: branding de la un capăt la altul
Pluginul trimite opt email-uri automate: confirmare înscriere, finalizare curs, temă trimisă/revizuită, bun venit membership, sesiune de tutoriat programată/reamintire/anulată. Toate email-urile sunt native WooCommerce și apar în WooCommerce > Setări > Email-uri și suportă personalizările standard de activare/dezactivare, subiect și titlu.
Dincolo de email-urile LMS personalizate, suprascriem header-ul global, footer-ul și stilurile CSS ale email-urilor WooCommerce. Fiecare email, confirmări de comandă, facturi, notificări de rambursare, folosește aceeași paletă zinc/verde cu brandingul companiei. Niciun mov sau albastru implicit WooCommerce nu se strecoară.
Ce am învățat
- WordPress este o platformă de aplicații capabilă când aduci propria arhitectură. Sistemul de hook-uri, abstracția bazei de date și infrastructura de utilizatori/roluri sunt solide. Ce nu-ți oferă WordPress este structura, pe aceasta trebuie să o aduci tu.
- Integrarea WooCommerce este alegerea corectă pentru plăți. Construirea unui checkout personalizat ar fi reprezentat luni de muncă pentru un rezultat inferior. WooCommerce gestionează gateway-uri, facturare, rambursări, cupoane și calculul taxelor. Noi ne conectăm doar la evenimente.
- Integrarea SDK-urilor terțe este fragilă. Saga Zoom ne-a costat 13 versiuni de iterații. Soluția finală, un tab nou curat, este mai simplă, mai fiabilă și a necesitat mai puțin cod decât oricare dintre abordările integrate.
- Auditurile de securitate se amortizează singure. Auditul cu 86 de probleme a descoperit race conditions care existau din v1. Fiecare pattern insert-first, fiecare actualizare atomică, fiecare descărcare tokenizată face sistemul mai demn de încredere.
- Constrângerile de design reduc deciziile. „Fără albastru, exclusiv majusculă la început de propoziție" sună restrictiv. În practică, înseamnă că fiecare funcționalitate nouă arată automat consistent fără revizuire de design. Constrângerea este sistemul de design.
APEX Digital LMS se află acum la versiunea 3.12, servind cursuri pentru Elite Business Club Romania. Gestionează cursuri video, sesiuni live, quiz-uri, examene, teme, tutoriat 1:1, membership-uri, diplome și o integrare completă WooCommerce, totul dintr-un singur plugin WordPress.
Dacă afacerea ta are nevoie de o platformă de învățare care să se potrivească exact cerințelor tale în loc să te forțeze într-un template, contactează-ne.
Ai nevoie de un plugin sau o platformă personalizată?
Construim pluginuri WordPress, integrări WooCommerce și aplicații web personalizate.