momente şi schiţe de informatică şi matematică
anti point—and—click

Introducere elementară în framework-ul PHP symfony

LAMP | PHP | symfony | virtual host
2010 jul

Cele patru manuale oferite de symfony constituie după toate aprecierile, cea mai bună documentaţie (care vizează şi nivelul elementar) realizată pentru un proiect "open-source"; dar nu ne propunem aici să traducem documentaţia.

După ce am parcurs mai mult sau mai puţin manualele, dar le-am şi întrebuinţat (lucrând Model Web pentru încadrarea şi orarul unei şcoli), am ajuns la ideea că lucrurile se puteau totuşi "prinde" şi pe calea care - paradoxal totuşi - este cea mai obişnuită: nu cu manualul începi, ci instalezi produsul şi frunzăreşti oleacă prin documentaţie, apoi lansezi o aplicaţie "default" şi observi ce se petrece, experimentând şi corelând cu "mici" investigaţii pe codul-sursă.

Lucrând astfel - folosind manualul nu ca pe o carte care trebuie citită în prealabil şi în mod liniar, ci pentru a căuta clarificări şi validări - se poate ajunge la acea înţelegere a principiilor de lucru specifice care este suficientă în fond, pentru a începe să foloseşti produsul conform propriilor necesităţi. Obişnuinţa cu acest stil de lucru se dovedeşte benefică apoi şi pe parcursul dezvoltării propriilor aplicaţii.

Asumăm aici un mediu de dezvoltare LAMP: Linux + Apache + MySQL + PHP şi vom folosi Git pentru a memora fişierele aplicaţiei împreună cu istoricul modificărilor efectuate asupra acestora.
Dar dincolo de aceste "asumări"… ne străduim ca de obicei, să nu mizăm din partea cititorului decât pe cunoştinţe şi deprinderi elementare.

Preliminarii; mici atenţionări

Precizăm pentru orice eventualitate, că Ubuntu (Linux) se poate instala foarte uşor, chiar şi peste un sistem Windows. Pentru a instala apoi cele necesare, se poate folosi meniul Applications -> Ubuntu Software Center, sau aplicaţia Synaptic Package Manager din meniul System -> Administration.

Însă pentru activitatea specifică programării este necesară obişnuirea cu "linia de comandă": selectaţi Applications -> Accessories -> Terminal ("Use the command line") şi trageţi-l în "Panel" (bara pe care este afişat meniul iniţial).

Faceţi click apoi, pe iconul "Terminal" creat astfel în Panel; tastaţi de exemplu git; probabil veţi obţine mesajul "The program 'git' is currently not installed", dar şi mesajul ajutător: "To run 'git'... install the package 'git-core'". Puteţi folosi desigur, "Ubuntu Software Center" - în caseta de căutare tastaţi 'git-core' şi folosiţi butonul "Install" (selectând primul pachet: "fast, scalable, distributed revision control system").

Dar puteţi folosi "terminalul", tastând sudo apt-get install git-core (a vedea manualul comenzii, tastând man apt-get). După instalare, tastaţi din nou "git"… (apoi şi man git).

Verificaţi dacă este instalat PHP (tastaţi: php -v); dacă nu este instalat, instalaţi-l folosind "apt-get" (la fel pentru oricare alt pachet necesar).

Desigur, dacă aţi preferat să instalaţi versiunea Ubuntu pentru limba română - atunci meniurile diferă de cele evocate mai sus (sunt traduse deja, la fel ca şi unele mesaje). Însă în general nu este tradusă decât partea de interfaţă, nu şi—de exemplu—paginile de manual, pe care programatorul le foloseşte curent pentru a se documenta sau pentru a-şi aminti cum să folosească diverse comenzi (prin man nume_comandă). Dacă e să folosiţi Linux ca programator (nu simplu utilizator), atunci este preferabil să instalaţi versiunea originală de Ubuntu şi dacă e cazul, să vă deprindeţi treptat cu limba engleză.

Subliniem ca şi în alte rânduri, că pentru programator este absolut necesară deprinderea "elementară" de a citi! Se poate constata acest fenomen: cei care au doar experienţa lucrului pe un sistem Windows - fiind deprinşi doar cu tehnica "point-and-click" - neglijează complet "terminalul", nu reuşesc să citească ce scrie pe ecran şi în general, pur şi simplu s-au dezobişnuit să mai citească (reducând folosirea calculatorului la apăsarea pe butoane, pe link-uri şi pe opţiuni de meniu). Ca să citescă, are nevoie de imprimantă - citind abia apoi, de pe hârtie; nu condamnăm, ci doar semnalăm fenomenul - în ideea că pentru a îndrepta lucrurile, întâi trebuie conştientizată comportarea curentă (inclusiv, rădăcinile ei).

Instalăm symfony

Scopul ar fi acela de a lucra cu ajutorul acestui framework, o anumită aplicaţie Web. Să înfiinţăm deci un director pentru fişierele acestei aplicaţii şi să prevedem un subdirector în care să instalăm symfony:

     mkdir -p webschool/lib/vendor

De regulă, vom reda ca mai sus comenzile tastate în "terminal"; linia de comandă "completă" arată de fapt astfel: vb@localhost:~$ mkdir -p webschool/lib/vendor (în cazul nostru).

Folosiţi man mkdir, pentru documentare. S-au creat directorul şi subdirectoarele specificate - puteţi folosi comenzile ls şi cd pentru a inspecta conţinutul unui director, respectiv pentru a schimba directorul curent.

Deschideţi acum un browser; puteţi selecta un alt "workspace" dintre cele 4 oferite de Ubuntu (vezi bara de la baza ecranului) şi puteţi folosi meniul Applications -> Internet -> Firefox Web Browser (trageţi-l în Panel, pentru a putea ulterior să-l deschideţi mai uşor).

Tastaţi în bara de adresă a browserului http://www.symfony-project.org/ şi ajunşi pe site-ul respectiv - faceţi click pe opţiunea de meniu Installation, apoi pe prima opţiune: "symfony 1.4".

Sub "Download as an Archive" găsiţi pachetul care trebuie descărcat; faceţi "click-dreapta" pe link-ul respectiv şi alegeţi "Copy Link Location" (desigur, alternativa obişnuită este de a downloada direct, pachetul respectiv).

Reveniţi acum în "terminal" (făcând click pe "workspace"-ul în care l-aţi deschis iniţial) şi intraţi în subdirectorul destinat instalării:

     cd webschool/lib/vendor

Tastaţi wget şi un spaţiu, apoi (împreună) tastele: SHIFT din dreapta şi INSERT — în mod normal ar trebui să se adauge (din "clipboard") link-ul copiat mai înainte, rezultând comanda:

     wget http://www.symfony-project.org/get/symfony-1.4.6.tgz

Folosiţi man wget pentru informare asupra utilitarului wget - "non-interactive download of files from the Web".

Extensia de mai sus, .tgz desemnează o arhivă în format tar care a fost în plus, comprimată. Utilitarul tar permite dezarhivarea pachetului respectiv:

     tar -zxvf symfony-1.4.6.tgz

Schimbăm denumirea directorului (man mv) rezultat prin dezarhivare şi eliminăm arhiva respectivă:

     mv symfony-1.4.6 symfony
     rm symfony-1.4.6.tgz

Primele observaţii şi deduceri

Directorul obţinut symfony conţine toate fişierele framework-ului; folosind ls, putem vedea că în principal ele sunt grupate în subdirectoarele data/ şi lib/.

Putem ignora în acest moment, subdirectoarele test/ şi licenses/ precum şi fişiere precum README - aflate şi acestea în directorul rădăcină symfony. Desigur, putem citi fişierul README folosind de exemplu gedit - editorul de text instalat "default" în Ubuntu (dar de data aceasta, README este extrem de concis).

Poate fi util pentru orientările ulterioare, să vedem întreaga structură de fişiere. În acest scop putem folosi utilitarul tree (poate fi instalat prin sudo apt-get install tree):

     mkdir -p ~/webschool/doc
     tree symfony > ../../doc/symfony-files.txt 

Directorul curent (unde tocmai am instalat symfony) este /webscool/lib/vendor/. Prima comandă creează subdirectorul doc/; a doua aplică tree pe directorul symfony şi redirectează ieşirea (folosind operatorul >), de la ecran la fişierul "symfony-files.txt", în subdirectorul tocmai creat "doc" (operatorul ../ permite accesarea directorului "părinte" faţă de cel curent). Fişierul rezultat se poate citi de exemplu cu gedit şi va fi utilă consultarea lui, din când în când.

În subdirectorul doc/ intenţionăm să păstrăm fişiere constituite în scopul documentării.

Sunt suficiente mici investigaţii de acest gen - cu ls, tree şi gedit - ca să ne dăm seama că lib/ este biblioteca principală a framework-ului, iar cele câteva fişiere din data/bin constituie nimic altceva decât o interfaţă cu caracter utilitar. N-avem acum decât să rulăm unele dintre cele câteva "mici" programe PHP existente în data/bin/, pentru a înţelege ce am putea face mai departe.

Invocând din directorul curent scriptul check_configuration.php:

      php symfony/data/bin/check_configuration.php

constatăm că se face o verificare a unor condiţii necesare funcţionării framework-ului, vizând (în acest moment) interfaţa PHP CLI ("Command Line Interpreter/Interface"). Dacă pe ecran vedem o atenţionare privitoare la absenţa unui program (de exemplu pentru "a PHP accelerator"), atunci putem să-l instalăm folosind apt-get. Deasemenea, se afişează calea pentru php.ini: "php.ini used by PHP: /etc/php5/cli/php.ini" şi se verifică anumite opţiuni de configurare; pentru eventuale corecturi, trebuie să edităm fişierul indicat:

     sudo gedit /etc/php5/cli/php.ini

User-ul obişnuit are drepturi depline - read, write, execute - doar în propriul director /home/userHome/ (referit de regulă prin ~/); pentru a accesa fişiere din alte zone ale sistemului de fişiere, el trebuie de obicei "să se dea drept altcineva", folosind sudo (vezi documentaţia, cu man sudo).

Folosind meniul "Search" din gedit, putem găsi uşor opţiunea din php.ini pe care trebuie să o modificăm. După ce facem toate corecturile necesare, salvăm fişierul şi închidem gedit; însă pentru a şi activa noua configuraţie tocmai instituită în php.ini, trebuie de regulă să şi restartăm serverul Apache:

     sudo /etc/init.d/apache2 reload

Putem apoi să reluăm check_configuration.php, pentru a verifica dacă nu ne-a scăpat încă ceva, faţă de corecturile tocmai întreprinse.

PHP are însă două fişiere de configurare php.ini (denumite la fel, dar aflate în locuri diferite); mai încolo, vom crea un "web-host" pentru aplicaţie şi va trebui să relansăm check_configuration.php din browser (nu din linia de comandă), pentru a verifica şi setările existente în al doilea "php.ini" (cel pentru "Apache HTTP web-server").

Directorul data/bin conţine încă nişte scripturi PHP, dar nu trebuie (sau nu se cade) să le executăm pe toate… Am ales mai sus - fără vreo ezitare - să executăm check_configuration.php pentru că însuşi numele scriptului indică despre ce este vorba şi pentru că verificarea prealabilă a configurării este absolut firească (şi nu poate avea - prin sine însăşi - "efecte laterale").

Deja putem sesiza direct grija deosebită pe care o are symfony faţă de alegerea denumirilor de fişiere. Filând fişierul constituit mai sus symfony-files.txt (a vedea fişierele din subdirectorul lib), se pot deduce şi unele convenţii de denumire (şi rămâne de căutat prin documentaţie pentru clarificare, la un anumit moment).

Câteva scripturi din data/bin conţin în denumire termenul "sandbox", despre care găsim lămuriri în data/bin/sandbox_skeleton/README. Noi ignorăm aici proiectul symfony sandbox, având în vedere mai ales faptul că de la bun început am ales altă cale de instruire… Dar putem observa acum că scriptul create_sandbox.sh explicitează deja ceea ce ar fi de făcut acum, de exemplu (extras din fişier):

     echo ">>> create a new project and a new app" 
     ${PHP} lib/vendor/symfony/data/bin/symfony generate:project ${SANDBOX_NAME} 

ceea ce spune că putem crea un nou proiect folosind scriptul PHP symfony, existent şi acesta în data/bin/.

Deschidem fişierul symfony (în gedit) şi măcar ne convingem că este vorba de un script PHP (deşi nu are ".php" în denumire). Parcurgând cele câteva linii ale sale, chiar dacă nu înţelegem în acest moment "ce fac" ele - putem conchide totuşi că scriptul respectiv nu are efecte laterale - adică nu va scrie nimic în fişierele existente; prin urmare putem să-l lansăm în execuţie fără nici o grijă (şi… "să vedem ce se întâmplă"):

     php symfony/data/bin/symfony

De "întâmplat", nu se întâmplă nimic - în schimb obţinem o pagină de "help" (care se dovedeşte extrem de utilă pe tot parcursul dezvoltării unei aplicaţii) şi care începe cu:

     Usage:
       symfony [options] task_name [arguments]

Deducem că symfony este un script utilitar, destinat să asigure îndeplinirea unei anumite sarcini "task_name". Derulând help-ul afişat, găsim că putem genera o "aplicaţie", un "modul", un "proiect" (şi un "task"):

     generate
       :app                Generates a new application
       :module             Generates a new module
       :project            Generates a new project

dar între aceste elemente există sigur, anumite condiţionări. Putem folosi opţiunea help pentru generate: şi putem constitui (în /doc) un fişier de documentare comun:

php symfony/data/bin/symfony help generate:module > ~/webschool/doc/help-generate.txt
php symfony/data/bin/symfony help generate:app >> ~/webschool/doc/help-generate.txt
php symfony/data/bin/symfony help generate:project >> ~/webschool/doc/help-generate.txt

Am folosit iarăşi, operatorul de redirectare a ieşirii - de data aceasta şi sub forma >>, pentru a adăuga la sfârşitul fişierului.

Din fişierul astfel creat, help-generate.txt, desprindem următoarele lămuriri:

     The generate:module task creates the basic directory structure
     for a new module in an existing application:

     The generate:app task creates the basic directory structure
     for a new application in the current project:

     The generate:project task creates the basic directory structure
     for a new project in the current directory:

din care deducem fireşte, că întâi trebuie creat un proiect (probabil, unic!); apoi, vom putea crea în cadrul acestui proiect una sau mai multe aplicaţii, iar în cadrul unei aplicaţii vom putea crea unul sau mai multe module.

E greu de crezut că ar putea coexista mai multe proiecte: "the basic directory structure" - care cu siguranţă înseamnă subdirectoare cu denumiri fixate - ar trebui să varieze de la proiect la proiect, contradictoriu cu faptul asumat că proiectul trebuie să fie "in the current directory".

Constituim un "repozitoriu" pentru fişierele noastre

Până acum nu am afectat structura de fişiere obţinută la instalare, decât prin faptul că am creat un subdirector suplimentar doc/ (altfel, ne-am asigurat mereu ca scripturile pe care le-am rulat nu au "efecte laterale"). Acum însă - vrând să creem folosind scriptul symfony, un proiect, poate o "aplicaţie" şi "module" - ar fi de prevăzut că pe parcurs vom dori la un anumit moment, să revenim la un context de lucru anterior - trebuind poate, să eliminăm (sau dimpotrivă, să recuperăm) unele fişiere (sau porţiuni de fişiere).

Pentru asemenea situaţii - în general, când avem de lucrat cu multe fişiere - se impune folosirea unui sistem automat de control şi urmărire a operaţiilor efectuate asupra fişierelor (cel puţin, vom evita astfel operaţiile manuale de "Copy&Paste"). Un asemenea sistem este git; ne-am referit la început, asupra instalării lui, iar acum vom folosi câteva comenzi specifice (vezi manualul, cu man git).

Revenim în directorul ~/webschool/ (eram în webschool/lib/vendor/) şi iniţializăm un "repozitoriu":

     cd ../../
     git init
     git add .
     git commit -m "Initial commit."

Termenul este "repository", iar http://translate.google.com/#en|ro|repository traduce prin "depozit", "repertoriu", etc. Am preferat o traducere de tip "furculision"… (vezi "Chiriţa în Iaşi" a lui Alecsandri).

git a constituit un subdirector .git în care va păstra indexat şi va şti să gestioneze toate modificările de fişiere din directorul curent (după marcarea fişierelor respective prin git add, urmată de "comiterea" lor).

Nu-i locul aici, dar dacă ar fi să lămurim principiile pe care se bazează git am proceda desigur astfel: am da comenzile de mai sus rând pe rând şi am observa ce modificări apar de fiecare dată în directorul .git; de exemplu, după "git init" vedem că .git/objects conţine două subdirectoare "vide", dar după "git add .", el conţine încă 256 de subdirectoare (şi deja nevide) denumite în hexazecimal "00", "01", ..., "ff" şi apare în plus fişierul ./index - ş.a.m.d. Desigur, observaţiile proprii şi mesajele apărute pe ecran trebuie corelate treptat cu manualul (man git).

Constituim un proiect şi un web-host

În acest moment, directorul nostru webschool (care este acum "directorul de lucru curent") este constituit din lib/vendor/symfony şi subdirectoarele "proprii" doc/ şi .git/.

În doc/help-generate.txt avem acum documentaţia necesară pentru "generate:project"; vedem acolo că trebuie să indicăm un nume pentru proiect - alegem aceeaşi denumire ca a directorului nostru de lucru:

     php lib/vendor/symfony/data/bin/symfony generate:project webschool

Mesajele rezultate pe ecran vizează crearea unor subdirectoare şi fişiere; să listăm structura da fişiere creată, dar exluzând .git/ şi symfony/* - putem folosi tree cu opţiunea L 3 (limitează adâncimea la trei) şi deasemenea, cu opţiunea F (adaugă "/" pentru "director" şi "*" pentru "fişier executabil"):

     vb@localhost:~/webschool$ tree -L 3 -F
     .
     +-- apps/
     +-- cache/
     +-- config/
     |   +-- databases.yml
     |   +-- doctrine/
     |   |   +-- schema.yml
     |   +-- ProjectConfiguration.class.php
     |   +-- properties.ini
     |   +-- rsync_exclude.txt
     +-- data/
     |   +-- fixtures/
     |       +-- fixtures.yml
     +-- lib/
     |   +-- form/
     |   |   +-- BaseForm.class.php
     |   +-- vendor/
     |       +-- symfony/
     +-- log/
     +-- plugins/
     +-- symfony*
     +-- test/
     |   +-- bootstrap/
     |   |   +-- functional.php
     |   |   +-- unit.php
     |   +-- functional/
     |   +-- unit/
     +-- web/
         +-- css/
         |   +-- main.css
         +-- images/
         +-- js/
         +-- robots.txt
         +-- uploads/
             +-- assets/

Observăm imediat următoarele: subdirectoarele apps/, cache/, log/ şi plugins/ (şi încă, test/functional/ de exemplu) sunt vide; s-a creat fişierul executabil symfony.

Deschidem întâi fişierul symfony, în gedit; are trei linii (dacă ignorăm directiva de "script PHP" şi comentariile) şi constatăm că ele se regăsesc aproape identic, în scriptul lib/vendor/symfony/data/bin/symfony pe care l-am folosit mai sus - încât putem fi siguri că îl putem executa fără grijă că s-ar produce efecte secundare.

Pentru a-l executa, putem folosi php symfony, dar şi mai scurt: ./symfony (înlocuind prefixul "php" cu "./") fiindcă prima linie din fişier #!/usr/bin/env php deja spune Linux-ului că trebuie să folosească PHP pentru a interpreta şi executa scriptul indicat, iar pe de altă parte, acest script se află chiar în directorul curent.

Să observăm apoi, că denumirile instituite sunt tipice (apar şi în alte framework-uri şi se folosesc de obicei, în proiectele pentru Web) şi în orice caz, sunt sugestive; apps este destinat desigur fişierelor constituente ale aplicaţiilor (să ne amintim din "help" că vom putea crea una sau mai multe "aplicaţii", folosind .symfony generate:app), iar config conţine fişiere de specificare a unor parametri de configurare (pentru întregul proiect, sau poate şi pentru aplicaţiile constitutive); etc.

web este cu siguranţă acel unic director (pe care Apache îl declară ca DocumentRoot) în care vor avea acces utilizatorii de pe Internet ai aplicaţiei: este evident că unui utilizator oarecare nu trebuie să i se permită accesul la fişierele interne ale aplicaţiei (cele care asigură funcţionarea aplicaţiei).

Mai devreme sau mai târziu - dacă este cazul să propunem aplicaţia respectivă spre utilizare prin Internet - va trebui să definim un "virtual host".

Să ne amintim întâi că am instituit subdirectorul doc/ cu scopul de a păstra într-un loc imediat accesibil nouă, anumite fişiere informative. Să observăm că dintre cele două fişiere înscrise acolo, help-generate.txt va fi de-acum inutil (pentru simplul motiv că vom putea obţine mai direct asistenţa necesară, tastând .symfony help generate:app, de exemplu) şi se cuvine să-l ştergem.

Dar este bine să păstrăm (în doc/ desigur) şi anumite fişiere de configurare "externe" aplicaţiei: fişierul în care definim "virtual-hostul" de care avem acum nevoie trebuie înscris în /etc/apache2/sites-available/, dar păstrând o copie a lui chiar în directorul nostru (aşadar, sub git) ne creem posibilităţi de revizuire ulterioară. Prin urmare, lansăm gedit, folosim opţiunea "New" şi înscriem textul următor:

<VirtualHost *:80>
    DocumentRoot /home/vb/webschool/web
    DirectoryIndex index.php
    ServerName www.webschool.d
    <Directory /home/vb/webschool/web>
        AllowOverride All
        Allow from all
    </Directory>

    Alias /sf /home/vb/webschool/lib/vendor/symfony/data/web/sf
    <Directory /home/vb/webschool/lib/vendor/symfony/data/web/sf>
        AllowOverride All
        Allow from all
    </Directory>
</VirtualHost>

Îl salvăm în doc/webschool şi apoi copiem fişierul în locul cuvenit (folosind cp şi prefixând desigur cu sudo):

     sudo cp doc/webschool /etc/apache2/sites-available

Pentru a termina constituirea "virtual-hostului" nostru, trebuie să mai adăugăm în fişierul /etc/hosts linia "127.0.0.1 webschool" (putem folosi sudo gedit /etc/hosts), să creem "symlink"-ul necesar în /etc/apache2/sites-enabled (folosind sudo a2ensite webschool) şi în final, să restartăm Apache.

Definiţia de mai sus specifică web/ ca fiind singurul loc accesibil publicului (toate cererile vor fi deservite numai prin intermediul lui "DocumentRoot") şi fixează ca adresă de acces http://www.webschool.d (folosim ".d" când e vorba de un host "local"; pentru un virtual-host care să poată fi accesat prin Internet de oriunde - trebuie obţinut în prealabil un anumit domeniu, etc.).

Ultima parte a definiţiei redate mai sus (cea referitoare la sf/) permite accesul "public" şi în symfony/data/web/sf/ - pentru situaţia când o cerere de resursă (din "DocumentRoot", desigur) ar necesita stiluri, cod javaScript sau imagini existente în sf/ (este de exemplu cazul aplicaţiei "default", la care ne referim mai jos).

În general vorbind, există două posibilităţi pentru a accesa resurse dinafara lui "DocumentRoot": fie prevedem asupra lor opţiunile AllowOverride All şi Allow from all (cum s-a şi făcut în cazul de faţă), fie prevedem "symlink"-uri la ele, în directorul web/ (procedeu folosit la instalarea plugin-urilor, cum poate vom arăta mai încolo).

Dar degeaba am încerca acum să folosim un browser, pentru a accesa adresa menţionată mai sus: în acest moment, niciunul dintre fişierele existente în web/ (doar trei: css/main.css, robots.txt şi .htaccess) nu are relevanţă ca punct de "intrare în aplicaţie".

Adăugăm o aplicaţie (posibil de accesat din browser)

Deocamdată - deservind aici un scop didactic - nu avem în vedere nici o aplicaţie; vrem doar să încercăm încă un pas, lărgind treptat înţelegerea contextului de lucru creat de symfony. Este de prevăzut că la un anumit moment vom vrea să eliminăm aceste încercări şi să revenim la această stare a proiectului. Ar fi momentul să marcăm în .git această stare (ca starea de bază, din care putem demara lucrul asupra unei aplicaţii "reale").

Dar mai întâi trebuie să observăm că există două subdirectoare speciale: cache/ şi log/, pe care am putea să le ignorăm (să nu le adăugăm în repozitoriu). Ele au mai degrabă scopuri utilitare; de exemplu, log serveşte de obicei pentru înregistrarea pe parcursul funcţionării, a diverselor "mesaje" cu informaţii privind accesarea aplicaţiei, sau vizând apariţia anumitor erori.

Subdirectorul cache are o raţiune mai complicată; redăm aici o idee. Când aplicaţia este accesată prima oară, framework-ul ("SF") va analiza fişierele de configurare (din toate subdirectoarele config/), va produce codul PHP corespunzător opţiunilor de configurare întâlnite şi de regulă, va memora în subdirectoare din cache/ codul respectiv; la a doua accesare (şi aşa mai departe, până la epuizarea unui anumit interval de timp, sau până la ştergerea subdirectorului creat) - SF nu va mai re-analiza fişierele de configurare pentru a obţine codul necesar, ci va folosi direct codul înscris anterior în cache/. Rezultă astfel o accelerare considerabilă a răspunsului.

Pentru ca git să ignore anumite fişiere, acestea trebuie declarate într-un fişier denumit .gitignore. Creem acest fişier în webschool/, înscriind pe câte o linie: cache/ şi respectiv log/. Apoi, marcăm fişierele de adăugat (cele din directorul curent ".", exceptând pe cele menţionate în .gitignore) şi "comitem":

     git add .
     git commit -m "Proiectul de bază; nu există nici o aplicaţie."

Tastăm întâi ./symfony help generate:app, pentru a ne aminti sintaxa comenzii; alegem fără pretenţii, numele deja sugerat de "help" pentru aplicaţie (anume, frontend):

     ./symfony generate:app frontend

Pe ecran sunt indicate subdirectoarele adăugate (la începutul rândului se specifică >>dir+), fişierele adăugate (sub >>file+) şi fişierele afectate (>>tokens indică o adăugare într-un fişier existent). În principal, sunt afectate directorele apps şi web (existente iniţial).

Aplicaţia pe care am denumit-o "frontend" este reprezentată în primul rând, prin subdirectorul frontend/ (intrat în apps/), al cărui conţinut îl putem lista folosind tree apps:

     apps
     +-- frontend
         +-- config
         |   +-- app.yml
         |   +-- cache.yml
         |   +-- factories.yml
         |   +-- filters.yml
         |   +-- frontendConfiguration.class.php
         |   +-- routing.yml
         |   +-- security.yml
         |   +-- settings.yml
         |   +-- view.yml
         +-- i18n
         +-- lib
         |   +-- myUser.class.php
         +-- modules
         +-- templates
             +-- layout.php

Evident, oricărei alte aplicaţii ("./symfony generate:app myApp") îi va corespunde o intrare apps/myApp/ având aceeaşi structură - conţinând în principal subdirectoarele config/, lib/, modules/ şi templates/.

Observăm deasemenea, că s-au creat două fişiere în directorul public web/, anume index.php şi frontend_dev.php. Aceasta înseamnă că acum putem accesa aplicaţia folosind un browser.

Intrăm deci în acel "workspace" în care vom fi deschis Firefox (sau alt browser) şi tastăm pe linia de adresă (ne mai uităm odată, la declaraţia ServerName din definiţia anterioară a virtual-hostului): http://www.webschool.d/. Reproducem aici rezultatul, prin imaginea următoare:

Primul lucru de făcut pentru a investiga mai departe, constă în a citi cu atenţie ceea ce scrie în pagina tocmai obţinută. Da, fiecare este convins că "ştie" să citească (păi deja în primul an de şcoală, am învăţat să citesc!) şi aproape că refuză ideea că "a citi" se învaţă şi la clasa a XII-a şi se învaţă mai ales, după toate clasele…

Iată - doar în treacăt - un mic test (de citire!): ce browser a folosit autorul, pentru a obţine pagina din imagine?
Nu Firefox?! Păi cum vine asta - a folosit Firefox cum zice, dar scrie Iceweasel? De ce oare?
Dar cum o fi obţinut imaginea reprodusă, din browser?
Dacă vă veţi fi pus şi aceste întrebări, atunci se cheamă că ştiţi să citiţi (şi veţi putea încă învăţa!).

Să observăm întâi că primul paragraf (titrat "Project setup successful") justifică prezenţa ultimei părţi din definiţia virtual-hostului (în care declaram Allow from All pentru /home/vb/webschool/lib/vendor/symfony/data/web/sf); altfel, fişierul imagine (corespunzător "logo"-ului de la începutul paginii redate) webschool/lib/vendor/symfony/data/web/sf/sf_default/images/sfTLogo.png nu ar fi putut fi accesat (nefiind în "DocumentRoot").

Al doilea paragraf este esenţial pentru noi: ne spune că există un "modul" denumit default şi care este responsabil de producerea paginii. Dar să citim cu atenţie: nu denumirea "default" dată modulului contează (aproape suntem atenţionaţi: "It will dissapear"), ci termenul "modul"!

Să corelăm cu faptul constatat anterior, că oricărei aplicaţii îi corespunde un subdirector apps/myApp/modules — ajungem astfel la această primă concluzie importantă: pagina returnată drept răspuns browserului este girată de fişierele existente în modulul respectiv.

Concluzia tocmai evidenţiată determină o altă întrebare importantă: de unde ştie serverul care modul dintre cele existente, trebuie folosit pentru constituirea corespunzătoare a răspunsului?
Vom vedea că şi răspunsul la această întrebare se poate "citi" din al doilea paragraf!

În sfârşit, lucrurile au început să se contureze! Este clar acum că metoda de investigaţie propusă - instalează produsul, experimentează, observă, caută în fişiere şi când ai nevoie, caută şi în documentaţie - are toate şansele să conducă la clarificarea aspectelor specifice produsului pe care vrem să-l folosim.
Doar că, totul depinde de evoluţia unei anumite deprinderi, sintetizată concis prin "a şti să citeşti".

Dar acum - înainte de a căuta modulul default şi a-l investiga, răspunzând desigur şi întrebărilor tocmai sesizate - să ne amintim totuşi că anterior, am amânat o anumită verificare…

Verificarea configurărilor php.ini

Aparent, n-ar mai fi nevoie de nicio verificare de configurări - de vreme ce "programul a mers", înseamnă că "totu-i OK"… Dar nu prea este aşa! Nu degeaba am fost atenţionaţi să verificăm nu numai php.ini pentru CLI (ceea ce am făcut imediat după instalare), dar şi fişierul de configurare PHP utilizat de Apache.

Să ne amintim unde se găsea fişierul check_configuration.php şi să-l copiem în directorul public web/ (altfel nu-l vom putea accesa din browser):

     cp lib/vendor/symfony/data/bin/check_configuration.php web/

Bineînţeles, am presupus că directorul curent este webschool/. Atenţie: a nu uita "/" de la sfârşit (altfel, fişierul ar fi copiat în directorul curent, primind numele "web" - nicidecum în directorul "web/", cu păstrarea numelui de fişier).

Acum fişierul poate fi lansat din browser: http://www.webschool.d/check_configuration.php. De data aceasta, vedem mesajul "php.ini used by PHP: /etc/php5/apache2/php.ini" şi dacă este cazul (vedem opţiuni care nu au "OK") folosim sudo gedit /etc/php5/apache2/php.ini pentru a face corecturile cuvenite (apoi neapărat, repornim Apache - cum am arătat deja anterior).

Desigur, după ce ne-am asigurat că "totu-i OK" putem şterge fişierul respectiv din web/.

E simplu de şters (vezi man rm)… dar mai bine să nu ne grăbim - avem un bun prilej pentru a sublinia (de exemplu pentru cazul de "începător") un aspect esenţial privitor la PHP (şi nu numai…).

Să deschidem în gedit fişierul respectiv; el începe cu tagul <?php şi conţine funcţii şi instrucţiuni PHP. Este evident că nu textul-sursă afişat acum de gedit este şi ceea ce afişează browserul.

Când browserul cere serverului un fişier PHP, lucrurile decurg astfel: serverul mobilizează componenta PHP, transferându-i fişierul respectiv; PHP analizează conţinutul fişierului şi recunoscând instrucţiunile specifice, le execută şi constituie treptat un fişier (X)HTML care în final este returnat serverului şi mai departe browserului care a iniţiat cererea respectivă.

Fişierul HTML primit de browser poate fi vizualizat ca atare, folosind de exemplu meniul "View/Page Source" al browserului (şi iarăşi se vede că textul vizualizat astfel nu este ceea ce se afişează iniţial).

Browserul la rândul său, primind fişierul HTML - mobilizează diversele sale componente (HTML, CSS, javaScript, etc.) pentru a interpreta textul-sursă primit şi a constitui "pagina" de ecran afişată în final.

Investigarea modulului "default": de la cerere - la răspuns

Să reluăm momentul în care am emis din browser această cerere: http://www.webschool.d/ şi am obţinut drept răspuns pagina redată în imaginea de mai sus.

Această cerere nu specifică nicio "resursă" (spre deosebire de a doua cerere: http://www.webschool.d/check_configuration.php - "resursa" vizată aici este fişierul "check_configuration.php").

Dar fişierul returnat drept răspuns (de fapt, doar angajat pentru a răspunde) va fi totuşi web/index.php - fiindcă acesta este specificat în directiva DirectoryIndex din definiţia virtual-hostului (drept "resursa" implicită, în caz că nu este explicitată una în cererea primită de la browser).

În acest moment, elementele pe care ne-am baza pentru a lămuri până la capăt lucrurile, sunt:
— fişierul index.php şi pagina HTML corespunzătoare
— paragraful al doilea de pe pagina redată:
This page is part of the symfony default module.
It will disappear as soon as you define a homepage route in your routing.yml.
— definiţia (şi conţinutul) modulului default; conţinutul fişierului routing.yml
În plus, să nu uităm că s-au creat două directoare "utilitare" cache/ şi log/, în care vom găsi eventual anumite informaţii de urmărire a lucrurilor, pe parcursul funcţionării aplicaţiei. Deasemenea, să ne amintim că dispunem de utilitarul .symfony, cu care putem experimenta anumite "task"-uri.

Să începem cu ceea ce mai devreme sau mai târziu, trebuie să facem: să căutăm definiţia modulului default, în lib/vendor/symfony/. Să ne amintim că imediat după instalare, noi am constituit fişierul doc/symfony-files.txt (am folosit tree şi am redirectat ieşirea), care listează toate fişierele din symfony/.

Deschidem în gedit acest fişier şi căutăm (folosind meniul "Search") cuvântul "default"; el apare de foarte multe ori şi trebuie să alegem - evident că ignorăm "default_icon.png", sau "sf_default" (în treacăt, să observăm că tree a înscris la sfârşit un sumar edificator: 787 directories, 2901 files).

Putem "rafina" căutarea, observând sintaxa impusă de tree: cuvântul nostru ar trebui să fie ramura iniţială a unui subarbore, deci să înceapă cu un spaţiu şi să se încheie cu "\n" (caracterul de "trecere pe următorul rând") - deci să repetăm căutarea, pentru " default\n". De data aceasta, chiar prima apariţie corespunde cu ceea ce căutăm (şi ramificaţia respectivă este symfony/lib/controller/default):

│   ├── controller
│   │   ├── default
│   │   │   ├── actions
│   │   │   │   └── actions.class.php
│   │   │   └── templates
│   │   │       ├── defaultLayout.php
│   │   │       ├── disabledSuccess.php
│   │   │       ├── error404Success.php
│   │   │       ├── indexSuccess.php
│   │   │       ├── loginSuccess.php
│   │   │       ├── moduleSuccess.php
│   │   │       └── secureSuccess.php
│   │   ├── sfController.class.php
│   │   ├── sfFrontWebController.class.php
│   │   └── sfWebController.class.php

Celelalte apariţii derivează pe ramuri ca plugins/sfDoctrinePlugin/; consultând eventual documentaţia, putem găsi că Doctrine serveşte pentru lucrul cu baze de date - ori aici nu am implicat încă, sub nici o formă, vreo bază de date (deci putem ignora acum, aceste alte apariţii ale termenului "default").

Prin urmare, modulul default conţine directoarele actions/ şi templates/. Aceasta trebuie să fie valabil pentru oricare modul ("default" însemnând în fond că "ţine loc" de modul, dacă acesta nu există).
Ne putem convinge de fapt, că oricare modul trebuie să conţină aceste directoare, anume - plecând de la ./symfony help generate:module (şi lansând apoi ./symfony generate:module frontend myModule, de exemplu).

Să deschidem acum (în gedit) fişierul default/actions/actions.class.php. Ne luăm permisiunea de a elimina unele comentarii şi de a reformata (dar fără a şi salva rezultatul - cel puţin, nu în lib/vendor/symfony/):

<?php

class defaultActions extends sfActions {
    
    // Congratulations page for creating an application
    public function executeIndex()  {  }

    // Congratulations page for creating a module
    public function executeModule()  {  }

    // Error page for page not found (404) error
    public function executeError404()  {  }

    // Warning page for restricted area - requires login
    public function executeSecure()  {  }

    // Warning page for restricted area - requires credentials
    public function executeLogin()  {  }

    // Module disabled in settings.yml
    public function executeDisabled()  {  }

}

Să nu ne cramponăm aici, de chestiuni de limbaj!
Ce este o "clasă" PHP, sau o "extindere" de clasă PHP, etc.; se poate consulta php.net/manual.
Ceea ce avem de observat este doar o chestiune notaţională (caz în care, observaţia directă trebuie şi confirmată prin consultarea documentaţiei privitor la convenţiile de notaţie folosite).

Denumirea "clasei de acţiuni" asociată modulului - în cazul nostru defaultActions, pentru modulul default - este formată prin alipirea cuvântului "Actions" la numele modulului. Iar pentru "acţiunile" componente, denumirea este formată prin prefixarea unei denumiri de funcţie - Index(), Login(), etc. - cu "execute".
Desigur, în documentaţie se pot găsi "amănunte", de exemplu privind folosirea majusculelor.

Pe de altă parte, să observăm legătura "acţiunilor" cu fişierele din default/templates/; acţiunii executeLogin() îi corespunde fişierul templates/loginSuccess.php, ş.a.m.d. De regulă, denumirea fişierului PHP corespunzător unei acţiuni este formată din numele propriu-zis al acţiunii (eliminând "execute" din denumirea acţiunii), scris cu litere mici şi sufixat cu "Success" (extinzând apoi cu ".php").
Dar "legătura observată" aici este una superficială, ţinând doar de notaţie. Legătura "reală" ar trebui să fie dată de faptul că fişierul PHP asociat acţiunii în templates/ este invocat într-un fel oarecare, din interiorul acţiunii - ori vedem că în cazul de mai sus, toate acţiunile sunt vide (de exemplu executeIndex() { } nu conţine nici o instrucţiune)! Ne bazăm deocamdată pe intuiţie, dar desigur că va trebui să clarificăm această legătură (fie şi apelând direct la documentaţie).

Deschizând eventual în gedit, putem constata că fişierele PHP corespunzătoare acţiunilor au pe prima linie:

<?php decorate_with(dirname(__FILE__).'/defaultLayout.php') ?>

iar altfel - conţin cod HTML obişnuit (şi foarte puţine linii cu instrucţiuni PHP).

Dar numai fişierul defaultLayout.php este un fişier HTML "complet": conţine pe prima linie obişnuita declaraţie <!DOCTYPE html PUBLIC...>, conţine apoi o secţiune <head> şi o secţiune marcată <body> (spre deosebire de fişierele asociate acţiunilor, care nu conţin aceste taguri).

Mai la începutul acestei secţiuni, am arătat că o cerere http://webschool.d/ va fi satisfăcută plecând de la fişierul web/index.php (despre care este suficient deocamdată, să vedem cu gedit că are propriu-zis trei linii şi de fapt, trei instrucţiuni PHP). Am văzut deasemenea, că această cerere produce ca răspuns o pagină care este girată de către modulul default/, aflat acum în discuţie.

Dar dacă vizualizăm sursa paginii afişate de browser (folosind meniul "View/Page Source", de exemplu) - atunci vom vedea că ea corespunde la două fişiere din default/templates/: pe de o parte, recunoaştem uşor elementele generate de fişierul defaultLayout.php; pe de altă parte, în secţiunea <body> vedem exact conţinutul HTML al fişierului indexSuccess.php.

Deci pagina arătată de browser rezultă prin "decorarea" fişierului corespunzător unei acţiuni, cu conţinutul generat de fişierul defaultLayout.php (cum şi sugerează linia PHP redată mai sus, decorate_with()).

Dar am reuşit să exemplificăm doar pentru acţiunea executeIndex(), ori "index" este el însuşi un element care este cotat de obicei, ca "default". Cum obţinem pagina corespunzătoare unei alte acţiuni, de exemplu pagina corespunzătoare acţiunii executeLogin()? Această pagină ar trebui să corespundă fişierului templates/loginSuccess,php, "decorând" desigur cu defaultLayout.php.

Dacă încercăm de exemplu, cererea http://webschool.d/login - atunci obţinem o pagină de eroare (eroarea fiind "Page Not Found"), despre care putem vedea că este rezultatul acţiunii executeError404() (însemnând conţinutul fişierului error404Success.php, decorat prin defaultLogin.php).

Rezolvăm dilema pe baza unei observaţii simple - reluând de fapt o întrebare pusă mai sus: de unde poate şti serverul care modul trebuie angajat pentru producerea paginii de răspuns? Pot exista mai multe module, fiecare cu actions/ şi templates proprii şi putem avea o acţiune denumită la fel în module diferite. De unde se poate şti că-i vorba de pagina corespunzătoare unui anumit modul şi unei anumite acţiuni din acesta, iar nu altuia?

Răspunsul este simplu, dacă ne gândim că pentru a obţine ceva, trebuie să formulezi corect cererea…

A cere o pagină sau alta - este de fapt decizia utilizatorului şi cum fiecare pagină corespunde unui anumit modul şi unei anumite acţiuni, rezultă că pentru a obţine o anumită pagină, însăşi cererea emisă de browser trebuie să precizeze modulul şi acţiunea respectivă.

Desigur, excepţia ar fi cererea http://webschool.d/, care nu specifică nici modulul "default", nici acţiunea "index" şi totuşi produce pagina corespunzătoare acestora… Vedem însă imediat că şi cererea "completă" http://webschool.d/default/index produce exact aceeaşi pagină - deci regula e valabilă.

Prin urmare, ca să obţinem pagina corespunzătoare acţiunii executeLogin() cum ne propusesem mai sus - trebuie să emitem http://webschool.d/default/login. Şi în acest caz, vizualizând sursele respective - putem constata că pagina arătată de browser este rezultatul "decorării" paginii generate de fişierul loginSuccess.php de către fişierul defaultLayout.php.

Posibilitatea de exceptare evidenţiată mai sus este asigurată de fişierul de configurare routing.yml, din apps/frontend/config/, în care găsim regula:

homepage:
  url:   /
  param: { module: default, action: index }

Această regulă specifică ce modul şi ce acţiune trebuie folosite, în cazul paginii "de bază" (cea corespunzătoare cererii http://webschool.d/).

Iar regula generală - confirmând aserţiunile de mai sus - apare deasemenea, în routing.yml:

default:
  url:   /:module/:action/*

cu semnificaţia străvezie că în general, o cerere trebuie să respecte şablonul (sau "regula de rutare") /cutareModul/cutareAction/listaParametri. De exemplu, http://webschool.d/myDefault/login/vlad/123456 ar corespunde acestei reguli, semnificaţia putând fi aceea că se cere autentificarea de către acţiunea login din modulul myDefault, a userului cu parametrii "username=vlad" şi "password=123456".

Revizuiri şi… neajunsurile activităţii de detectiv

Am văzut mai sus, că acţiunile din default/actions/ sunt vide (metodele definite în clasa PHP defaultActions nu conţin instrucţiuni de executat). Am sesizat anumite semnalmente comune, între denumirile acţiunilor şi respectiv ale fişierelor din defaults/templates/ şi apoi am văzut (folosind gedit, "View->Page Source") că această legătură nu este de loc întâmplătoare: pagina finală returnată browserului este generată de un "decorator" Layout.php (prezent şi acesta, în templates/) împreună cu acel fişier din templates, care corespunde acţiunii implicate de cererea la care se răspunde (dat fiind că această cerere trebuie să precizeze cumva, care modul şi acţiune trebuie mobilizate pentru obţinerea răspunsului cerut).

Să observăm acum şi două neajunsuri ale demersurilor pe care le-am făcut (ca un adevărat detectiv, probabil). Mai întâi - am "lucrat" şi am dedus totul numai pe seama modului predefinit default/, extrapolând însă la cazul oricărui alt modul, pe baza principiului literal default = ţine loc de ceva neexistent (în cazul nostru, ţine loc de un modul oarecare). Probabil că n-ar trebui aici să exagerăm cu "rigoarea" şi să acceptăm că aşa şi este; totuşi, "principiul" invocat (şi "literal", numai) este dintre acelea "nescrise", fiind astfel şi vulnerabil.

Dar mai este un aspect care ar impune totuşi, o anumită rigoare: am văzut că putem folosi "task"-ul ./symfony generate:module pentru a defini un modul oarecare, dar am văzut că pentru aceasta trebuie să precizăm cărei aplicaţii din apps/ îi este destinat modulul respectiv. Ori modulul default este (pre)definit global, el nu face parte propriu-zis din nici o "aplicaţie" existentă (doar că folosirea lui necesită înfiinţarea prealabilă a unei aplicaţii).

Raţiunea existenţei acestui modulul global nu este deloc, aceea de "demo", sau de material documentar. Acţiunile şi şabloanele de pagină din default/ vor fi angajate în acele momente în care SF nu poate executa cerinţa curentă, de exemplu pentru că modulul sau/şi acţiunea specificate nu există (caz în care SF va apela la default/executeError404), sau fiindcă pe modulul respectiv există anumite configurări care restricţionează accesarea lui (caz în care SF ar apela de exemplu executeLogin() din default/).

Prin urmare "pentru liniştea noastră" - este bine să definim un modul obişnuit oarecare, în cadrul aplicaţiei frontend (deja definită) şi să verificăm pe cazul acestuia, aserţiunile extrapolate mai înainte de la default.

Al doilea neajuns ţine de tehnica obişnuită şi de… prudenţă. Am evidenţiat mai înainte că deşi metodele clasei defaulActions sunt vide, ele şi tocmai ele determină conţinutul paginii finale de returnat browserului ca răspuns. Vom putem imagina anumite experimente pentru a ne clarifica mecanismul - dar aceasta implică şi să înscriem unele secvenţe de cod în corpul unor metode din defaultActions. Ori această clasă PHP (ale cărei metode am vrea acum să le modificăm) aparţine codului nativ al framework-ului şi în general e bine nici să nu te gândeşti să modifici codul intern al unei biblioteci "străine" propriului cod.

Avem astfel, încă un argument de a implica un modul propriu (evitând să mai lucrăm direct pe modulul default/).

Constituim o ramură git pentru experimente

Să ne amintim că am dat fişierele noastre în grija lui git (înfiinţând un "repozitoriu" şi folosind git add) şi am consemnat deja, starea actuală a acestora (folosind git commit). Având acum de făcut un anumit experiment -implicând modificarea temporară a unor fişiere - înfiinţăm o ramură de dezvoltare, fie my_default:

     git branch my_default
     git checkout my_default

Avem acum două "ramuri" de lucru şi deja am comutat pe cea tocmai iniţiată:

     git branch
  master
* my_default

Tot ce se lucrează mai departe, va fi consemnat de git în contul ramurii actuale (cea marcată cu '*').
git checkout permite comutarea de la o ramură la alta, mascând complet diferenţele; de exemplu, dacă adăugăm un fişier nou - acesta va exista numai pe ramura actuală, nu şi pe alte ramuri.
Aici nu va fi cazul, dar dacă vom dori - vom putea şi unifica la un moment dat, ramurile respective.

Desigur, putem crea un modul propriu prin .symfony generate:module frontend mydefault; dar aşa, va trebui să scriem noi înşine diverse "acţiuni" şi şabloanele PHP asociate acestora în templates/ (implicând o divagaţie prea mare). Este mult mai simplu şi mai direct să copiem pur şi simplu modulul predefinit default:

     cp  -r  lib/vendor/symfony/lib/controller/default  apps/frontend/modules

Sunt de făcut două-trei schimbări de denumire, "default" -> "mydefault"; întâi, pentru numele modulului:

     cd  apps/frontend/modules
     mv  default  mydefault

Apoi, deschidem în gedit fişierul mydefault/actions/actions.class.php şi modificăm numele clasei PHP (din "defaultActions" în mydefaultActions).

Dacă vrem, putem să modificăm şi denumirea fişierului templates/defaultLayout.php (să încercăm chiar myLayout.php, neincluzând şi denumirea modulului!) - astfel "scăpăm" complet de termenul "default" şi eliminăm posibilitatea unor îndoieli ulterioare. Doar că, trebuie să ţinem seama că acest fişier "decorează" celelalte fişiere din templates/, încât denumirea trebuie modificată şi în conţinutul acestora:

     cd  mydefault/templates
     mv  defaultLayout.php  myLayout.php

     perl -p -i -e "s/defaultLayout.php/myLayout.php/" *

Ultima linie reprezintă o metodă uzuală pentru a înlocui un text, în mai multe fişiere; în cazul de faţă se înlocuieşte "defaultLayout.php" (numai prima apariţie) cu "myLayout.php" în fiecare fişier (caracterul "*" semnifică "oricare") din directorul curent.
Metoda este populară sub denumirea "easy as pie", făcând apropo de opţiunile implicate; "perl" este un limbaj şi ar fi de văzut eventual măcar Perl, având în vedere că este integrat în orice distribuţie de Linux.

Dar… n-am scăpat (cum ziceam mai sus) de "default" (şi merită probabil să evocăm "filozofia" acestei constatări: a citi înseamnă a reciti). Era să uităm de regula de rutare din frontend/config/routing.yml, care impunea ca parametri "module: default" şi "action: index"; să o modificăm acum, dar chiar aşa:

homepage:
  url:   /
  param: { module: mydefault, action: login }

însemnând că "apelul" http://webschool.d/ va trebui să returneze pagina generată de mydefault/templates/loginSuccess.php şi decorată de mydefault/templates/myLayout.php şi nu pe aceea care ar rezulta din lib/vendor/symfony/.../default/templates/indexSuccess.php (şi decoratorul "defaultLayout.php").

Trebuie să ne amintim şi nişte precizări (… făcute cam demult) privind rolul subdirectorului cache/: păstrează între altele codul deja compilat, al parametrilor de configurare. Aceasta înseamnă ca pentru a forţa SF să recompileze fişierul routing.yml (pentru a activa noua configurare, în locul celeia existente în cache/) ar trebui poate, "să ştergem" conţinutul directorului cache/; dispunem de un "task" în acest scop:

     ./symfony cache:clear

Paginile girate de mydefault/ ar arăta absolut la fel cu cele din default, fiindcă nu am făcut nici o modificare de conţinut HTML, în fişierele copiate. Ca să "personalizăm" minimal paginile girate din mydefault, să deschidem în gedit fişierul myLayout.php şi să inserăm ceva, de exemplu un element <h1>:

...
  <h1 style="color:blue;">mydefault</h1>
  <?php echo $sf_content ?>
...

Astfel, toate paginile girate de mydefault/ urmează să fie decorate suplimentar cu subtitlul "mydefault".

Imaginea de mai jos reproduce două rezultate: după http://www.webschool.d/, când modulul implicat este mydefault/ (iar acţiunea este loginSuccess.php - conform definiţiei "homepage" tocmai înregistrate în routing.yml) şi respectiv, după http://www.webschool.d/default/login care specifică direct modulul şi acţiunea. Rezultatele sunt identice (exceptând subtitlul "mydefault"), dar este clar acum că ele provin din locuri diferite.

Putem constata la fel - cu http://www.webschool.d/login, sau http://www.webschool.d/error404, etc. - faptul că dacă specificăm în cerere numai acţiunea ("login", "error404", etc.), nu şi modulul - atunci răspunsul este girat de modulul "global" default (respectând asumarea "default = ţine loc de"). Şi desigur, http://www.webschool.d/mydefault/error404 va produce pagina girată de mydefault (cea decorată cu "mydefault").

Încheiem pregătirea experimentului, consemnând în git starea actuală a fişierelor:

     git add .
     git commit -m "modulul mydefault, pentru experimente"

Comutând (numai temporar) pe ramura "master" - prin git checkout master - am putea constata că nu mai există "mydefault/", iar definiţia de "homepage" din routing.yml a redevenit cea iniţială.

O mică investigaţie a fluxului de execuţie

În frontend/modules/ avem acum un modul obişnuit, mydefault/ - cu toate că acţiunile existente (şi şabloanele de pagină aferente) nu-s altceva, decât copii ale celor existente în modulul global controller/default/. Am văzut mai sus, că aceste copii din mydefault/ pot servi pentru a "personaliza" acţiunile globale din default/ (am adăugat un subtitlu, în myLayout.php) - dar aceasta depinde şi de context.

De exemplu, http://www.webschool.d/mydefault/signin care specifică modulul nostru şi o acţiune inexistentă, provoacă o "pagină de eroare" pe care se recunoaşte imediat fişierul templates/error404Success.php - numai că nu "copia" din mydefault (fiindcă lipseşte "personalizarea"), ci din modulul global default (în principiu, erorile şi trebuie, să poată fi controlate de pe un nivel mai înalt decât al unei componente particulare).

Dar "pagina de eroare" se rezumă în cazul de faţă, la "Page Not Found: The server returned a 404 response." - nu este una "veritabilă" (care să fie utilă pentru investigaţii)…

Să ne amintim iar, că totul începe cu executarea fişierului index.php din directorul web/; să ne amintim că web/ este "DocumentRoot"-ul nostru (singura intrare publică în aplicaţie) şi că "index.php" a fost creat odată cu generarea aplicaţiei apps/frontend/ (când am folosit "task"-ul .symfony generate:app). Dacă nu l-am modificat (ceea ce era posibil, desigur), "index.php" conţine aceste trei "instrucţiuni":

require_once(dirname(__FILE__).'/../config/ProjectConfiguration.class.php');
$configuration = ProjectConfiguration::getApplicationConfiguration('frontend', 
                                                                   'prod', false);
sfContext::createInstance($configuration)->dispatch();

şi-l putem citi fără pretenţii: întâi se încarcă fişierul care defineşte clasa PHP ProjectConfiguration; acest fişier se află în subdirectorul config/ din directorul proiectului (fiindcă __FILE__ este "index.php", apoi dirname("index.php") este web, iar web/../config înseamnă "accesează directorul părinte al lui web/ şi intră în config/").

Deschizând config/ProjectConfiguration.class.php, putem intui că se asigură "autoîncărcarea" claselor SF de bază (prin apelul sfCoreAutoload::register();). Între clasele SF astfel încărcate, trebuie să fie şi sfContext, a cărei metodă ->dispatch() este invocată în finalul fişierului "index.php".

Căutând eventual în doc/symfony-files.txt, găsim symfony/lib/util/sfContext.class.php, în care vedem chiar la început, un comentariu edificator:

     sfContext provides information about the current application context, such as
     the module and action names and the module directory. References to the
     main symfony instances are also provided.

Deducem că ->dispatch() transferă execuţia către modulul şi acţiunea indicate în cerere, dar "about the current application context". Fără îndoială - contextul curent al aplicaţiei este dat de lista ('frontend', 'prod', false) cu care este apelată metoda getApplicationConfiguration, în a doua "instrucţiune" din index.php.

Revenind la fişierul config/ProjectConfiguration.class.php pe care l-am deschis mai înainte, putem vedea că această clasă ProjectConfiguration (de care este legată prin :: metoda menţionată mai sus), extinde clasa sfProjectConfiguration; fişierul corespunzător acesteia poate fi identificat în symfony/lib/config/ şi găsim acolo, privitor la lista de parametri menţionată:

   * Returns a sfApplicationConfiguration configuration for a given application.
   *
   * @param string            $application    An application name
   * @param string            $environment    The environment name
   * @param Boolean           $debug          true to enable debug mode

Deci asocierile făcute sunt: $application = 'frontend', $environment = 'prod' şi $debug = false. Mai vedem şi că putem "enable debug mode", adică putem să obţinem pagini de eroare "veritabile" - comutând parametrul $debug. Prin urmare: modificăm fişierul index.php, înlocuind false cu true în cadrul celei de-a doua "instrucţiuni" (şi îl salvăm). Reluând acum din browser, obţinem într-adevăr o pagină de eroare (care nu se rezumă la "Page Not Found", ca în cazul anterior când aveam $debug = false):

Sub stack trace vedem etapele principale parcurse în scopul deservirii cererii menţionate în bara de adresă. Etapele respective (aici, în număr de 4) sunt numerotate în ordinea inversă momentului desfăşurării (ultima efectuată este cea menţionată în vârful stivei).

Prima etapă (cea numerotată cu 4) constă în executarea fişierului index.php din directorul web/, încheiată cu apelul ->dispatch() cum am discutat mai sus.

După ce a parcurs paşii 4 şi 3 - SF deţine deja o gamă de informaţii privitoare la aplicaţia frontend (aceasta fiind cea menţionată în index.php), inclusiv asupra modulelor şi acţiunilor existente (deasemenea, privitoare la configurările actuale, inclusiv privind opţiunile de rutare a cererilor).

La pasul 2 se încearcă lansarea acţiunii signinSuccess() din modulul mydefault ceea ce provoacă (fiindcă acţiunea respectivă lipseşte) excepţia sfError404Exception indicată la 1 (linia 196). Comentariul "create an instance of the action" (linia 199) arată că dacă nu s-ar fi depistat această excepţie, atunci execuţia ar fi continuat cu lansarea acţiunii indicate în cerere; excepţia respectivă a determinat însă oprirea execuţiei şi afişarea stivei de execuţie, pe "pagina de eroare" (ţinând cont că $debug = true).

Clasele PHP care gestionează excepţiile, precum şi şabloanele de "pagină de eroare" se găsesc în symfony/lib/exception/. De exemplu, sursa paginii redate mai sus este exception/data/exception.html.php.

Paginile de eroare, dacă afişează şi "stack trace" - sunt realmente de mare ajutor atât pentru investigarea produsului respectiv, cât şi pentru depanarea unei aplicaţii. Dar SF oferă şi un mecanism special conceput pentru procesul de dezvoltare a aplicaţiei (separând "producţia" de "dezvoltare").

Mai întâi, se poate configura o "ramură" (cum am zice în git) pentru "producţie", o alta destinată activităţii de dezvoltare curentă, ş.a.m.d. Am văzut mai sus că în fişierul index.php este angajată configurarea existentă pentru aplicaţia frontend, pe "ramura" prod (sau citând de mai sus, $environment = 'prod').

Dar în momentul generării aplicaţiei frontend, în web/ a fost creat nu numai fişierul index.php, ci şi fişierul frontend_dev.php (menţionat în treacăt, la vremea respectivă), pentru "ramura" dev.

frontend_dev.php conţine şi el cele trei "instrucţiuni" din index.php, dar "contextul curent al aplicaţiei" este acum ('frontend', 'dev', true) ceea ce înseamnă că - fără să mai fie nevoie de modificarea parametrului $debug, ca în cazul lui "index.php" - va produce dacă este cazul, pagini de eroare "veritabile". Iar execuţia va decurge la fel ca în cazul accesării lui index.php - de exemplu, http://webschool.d/frontend_dev/mydefault/signin conduce în acelaşi mod şi la aceeaşi pagină de eroare ca mai sus (înlocuind însă "index.php" cu "frontend_dev.php").

Dar acum apare în plus - şi pe paginile obişnuite şi pe cele "de eroare" - un mecanism de urmărire special creat pentru dev, având ca interfaţă grafică o bară de instrumente conţinând link-uri pentru config, view, logs, etc.:

Click pe opţiunea de meniu config conduce la afişarea parametrilor de configurare pentru "Request", "Response", "User", etc. Click pe view vizualizează parametrii primiţi de şablonul de pagină de la acţiunea pe care o deserveşte. Click pe logs expune amănunţit "etapele" execuţiei.

Acţiuni şi şabloane de pagină

Am observa întâi că şi în programe obişnuite (în orice limbaj), întâlnim "acţiuni" şi "şabloane de pagină" (sau le putem recunoaşte ca atare). Iată un exemplu tipic, în C++:

#include <iostream>
// #include <string>   // necesar pe compilatoare C Microsoft
using namespace std;

void scrie_nume( string nume ) {
	cout << "Numele cerut este: ";
	cout << nume << '\n';
}

void get_nume() {
	// obţine de pe undeva, datele cerute
	string first = "Jan Ion", last = "Popesco";

	// constituie valoarea de transmis afişării
        string nume = last + ", " + first;

	scrie_nume( nume );
}

int main() {
	get_nume();
	return 0;
}

Aici, main() primeşte "cererea" get_nume(), determină intern punctul de intrare corespunzător şi pune în execuţie "acţiunea" respectivă; aceasta determină datele de care are nevoie, le prelucrează şi constituie variabilele necesare "şablonului de pagină" scrie_nume() (transferându-i controlul în final).

La fel se petrec lucrurile şi în SF, mutatis-mutandis. Şi n-avem nevoie deocamdată, să inventăm: cum am văzut şi mai sus, găsim "şabloane de pagină" preconstituite, în symfony/lib/exception/data/. De exemplu, în exception/data/error.html.php avem:

    <div class="sfTMessageWrap">
      <h1>Oops! An Error Occurred</h1>
      <h5>The server returned a "<?php echo $code ?> <?php echo $text ?>".</h5>
    </div>

Evident că acţiunea care a condus la această pagină de eroare trebuie să fi transmis o valoare pentru variabila $code şi una pentru variabila $text, încât browserului i se va transmite de exemplu,
<h5>The server returned a 404 response.</h5>
($code = '404', $text = 'response').

Dar un exemplu şi mai bun este fişierul exception/data/exception.txt.php: conţine şi o variabilă "tablou", este concis (are 10 rânduri şi conţine "text", nu şi HTML) şi în plus, putem recunoaşte că este vorba de afişarea informaţiilor care apar pe pagina de eroare redată deja, mai sus.

Să facem un mic experiment: copiem conţinutul acestui fişier (renunţând doar la ultimele două linii - afişarea versiunii pentru SF şi PHP) şi-l înscriem ("paste") în interiorul fişierului mydefault/templates/error404Success.php (în locul listei de definiţii care exista iniţial); adăugăm doar <br> în locurile cuvenite, încât acum avem:

<?php decorate_with(dirname(__FILE__).'/myLayout.php') ?>

<div class="sfTMessageContainer sfTAlert"> 
  <?php echo image_tag('/sf/sf_default/images/icons/cancel48.png', 
                       array('alt' => 'page not found', 'class' => 'sfTMessageIcon', 
                             'size' => '48x48')) ?>
  <div class="sfTMessageWrap">
    <h1>Oops! Page Not Found</h1>
    <h5>The server returned a 404 response.</h5>
  </div>
</div>

[exception]   <?php echo $code.' | '.$text.' | '.$name ?>
<br>[message]     <?php echo $message ?>
<?php if (isset($traces) && count($traces) > 0): ?>
<br>[stack trace]
<?php foreach ($traces as $line): ?>
  <br><?php echo $line ?>

<?php endforeach; ?>
<?php endif; ?>

Linia <?php echo $code.' | '.$text.' | '.$name ?> ar produce desigur începutul paginii de eroare arătate mai sus: 404 | Not Found | sfError404Exception (după care ar urma $message = 'Action "mydefault/signin" does not exist.' şi apoi, lista "stack trace" descrisă anterior). În realitate, nu se "produc" toate acestea, ci doar:

semnalând fireşte, faptul că variabilele folosite în error404Success.php nu sunt definite.

Click pe logs ne dezvăluie "fluxul de execuţie"; întâi este apelată "acţiunea" executeError404(), apoi se "tencuieşte" şablonul de pagină error404Success.php ("decorat" apoi cu myLayout.php):

executeError404() ar fi trebuit să pregătească şi să transmită variabilele necesare şablonului de pagină (altfel, acestea sunt notificate ca "Undefined variable", cum vedem mai sus). Ori toate "acţiunile" din mydefault/actions/actions.class.php au rămas vide, până în acest moment.

Desigur, rămâne să scriem în executeError404() codul necesar pentru a seta variabilele respective ($code, $text, $name, $message şi $traces). Dar cum să facem aceasta?

În cazul nostru, particularitatea este că nu ne interesează numai "cum să facem" - că atunci am cita sau am trimite la manual… Este benefică totdeauna, încercarea de a evidenţia şi mecanismele aferente.

Mecanismul de transmitere a variabilelor

Vom vedea că este vorba de o idee specifică dezvoltării orientate pe obiecte (indiferent de limbaj): se defineşte o clasă "utilitară" de obiecte - să-i zicem după SF, "ParameterHolder" - cu metode pentru înscriere şi regăsire de "variabile" sau proprietăţi (set(nume, valoare), get(nume)).

Clasele care definesc obiectele "de lucru" specifice aplicaţiei - cum sunt în SF, sfContext, sfActions, sfView, etc. - au grijă să includă ca "membru" şi un obiect (sau o referinţă la un obiect) "ParameterHolder".

Atunci, pentru ca un obiect (fie sfActions) să comunice unui alt obiect (din sfView, de exemplu) nişte parametri - trebuie ca obiectul emitor să îi înscrie într-un obiect "ParameterHolder" propriu şi posibil de referit de către obiectul "destinatar" (care astfel, îi va putea accesa la capătul celălalt).

Să plecăm de la mydefault/actions/actions.class.php, unde avem definiţia clasei PHP care conţine "acţiunile" noastre (inclusiv executeError404() asupra căreia am ales să experimentăm):

 class mydefaultActions extends sfActions { ... }

Relaţia dintre clasa de "acţiuni" mydefaultActions şi clasa sfActions este una de "moştenire" (impusă prin specificaţia extends): toate variabilele şi metodele care sunt declarate în clasa "parent" sfActions prin atributul public, sau prin atributul protected, sunt accesibile din oricare acţiune a clasei "child" mydefaultActions.

Mai departe, accesând fişierele corespunzătoare din symfony/lib/action/ găsim succesiv definiţiile:

 abstract class sfActions extends sfAction { ... }
 abstract class sfAction extends sfComponent { ... }
 abstract class sfComponent { ... }

Prin urmare, "de bază" este clasa sfComponent; toate variabilele şi metodele acesteia - dacă sunt calificate prin public sau protected - sunt accesibile din acţiunile noastre. Conţinutul acestei clase ar trebui să permită şi răspunsul la întrebarea "cum se transmit variabile din acţiuni, spre şabloanele de pagină asociate".

Redăm aici numai ceea ce este strict legat de întrebarea pusă:

abstract class sfComponent
{
    protected
/* porţiune ignorată aici */
        $varHolder = null;
/* porţiune ignorată aici */
      
    public function initialize($context, $moduleName, $actionName)
    {
/* porţiune ignorată aici */
        $this->varHolder = new sfParameterHolder();
    }

    /**
     * Sets a variable for the template.
     * ...
     */
    public function setVar($name, $value, $safe = false)
    {
        $this->varHolder->set($name, $safe ? new sfOutputEscaperSafe($value) : $value);
    }
/* porţiune ignorată aici */
}

În obiectul curent (referit prin $this) este injectată variabila $varHolder, iniţializată (folosind operatorul new) ca referinţă la un "container de parametri" sfParameterHolder (clasă definită în lib/util/).

Metoda setVar() va putea fi utilizată din cadrul unei acţiuni, pentru a "transmite" parametri - anume sub forma: $this->setVar(nume, valoare) (dar vom vedea îndată că putem folosi şi o "scurtătură").

Astfel că - revenind la experimentul iniţiat mai sus - putem scrie în mydefault/actions/actions.class.php:

  public function executeError404()
  {
    $this->setVar('code', 'XXX');    
    $this->setVar('text', 'semnificaţia umană');
    $this->setVar('name', 'sfClassName');
    $this->setVar('message', 'Acţiunea mydefault/error404Success()');
    $this->setVar('traces', array('Etapa1'=>'front controller',
                                  'Etapa2'=>'dispatch()',
                                  'Etapa3'=>'lansează acţiunea'));    
  }

şi obţinem de data aceasta, această pagină (click view arată şi parametrii primiţi de "şablonul de pagină")

Dar în clasa sfComponent găsim şi o "scurtătură" pentru $this->setVar(nume, valoare):

  public function __set($key, $value)
  {
    return $this->varHolder->setByRef($key, $value);
  }

care angajează "metoda magică" __set() (vezi manual PHP/oop5.magic), permiţând să scriem de exemplu $this->code = 'XXX' (în loc de $this->setVar('code', 'XXX')). Funcţia setByRef() este "injectată" de clasa sfParameterHolder, dar numele spune deja despre ce este vorba…

De ce şi acţiuni şi şabloane de pagină?

Am observat că în principiu, întâlnim "acţiuni" şi "şabloane de pagină" (sau "de afişare") în orice program şi am ilustrat mai sus, printr-un scurt program C++. Dar n-ar fi nici o surpriză, reproşul aparent just: puteai face totul în main() - citeşti numele prenumele şi afişezi în ce fel vrei, ce-ţi trebuie funcţii separate pentru asta?!.

Şi analog: ce-ţi trebuie actions şi templates - adică ce, dintr-o "acţiune" nu poţi foarte bine să şi afişezi?! Bun, acţiunea determină datele, dar de ce să nu le şi afişeze direct, fără a mai apela la "şabloane de pagină"?!.

Luând în seamă aceste întrebări, să revedem câteva lucruri binecunoscute. Ştim că pentru ca să "afişezi în ce fel vrei" din main() - fiind vorba de C++, dar cam la fel stau lucrurile şi în alte limbaje - trebuie să prevezi la început #include <iostream>; aceasta înseamnă că de fapt, foloseşti deja o bibliotecă separată.

Ştim desigur şi raţiunea pentru care nu "facem totul în main()", ci prevedem funcţii separate; de exemplu, dacă am vrea să scriem rezultatele într-un fişier HTML, atunci va trebui să modificăm numai funcţia care se ocupă de "afişare" (scrie_nume(), în exemplul dat mai sus).

Ştim iarăşi toţi - graţie celor care decid ce să se facă şi ce să se folosească în principal în şcolile noastre - ce este un fişier .DOC sau mă rog, un "document Word": este un document monolitic care cuprinde tot ce trebuie pentru ca să-l deschizi cu "Microsoft Office Word" şi să-l tipăreşti la cutare imprimantă şi în cutare format de pagină.

Dar ştim că avem de-a face şi cu alt gen de documente; dacă faci click-dreapta pe o pagină Web, ai opţiunea "Save Page As..." şi dacă salvezi cu opţiunea implicită "Web Page, complete" - poţi observa că odată cu pagina propriu-zisă, se creează şi un director conţinând diverse alte fişiere (cu extensii ca .css, .js, .gif, etc.). Deci un "document Web" deja nu mai este un document monolitic, putând fi constituit din mai multe părţi separate.

Avantajul separării lucrurilor este imens: fiecare parte poate fi dezvoltată, depanată, înlocuită aproape independent de celelalte părţi (trebuind doar să fie cunoscuţi parametrii de legătură cu alte părţi).

Dacă nu-ţi convine <iostream> în programul tău C++ n-ai decât să înlocuieşti cu <stdio> (şi să modifici funcţia de afişare ca să folosească "print" în loc de "cout"); dacă vrei un alt layout al paginii Web, n-ai decât să modifici conţinutul fişierului CSS corespunzător; dacă display-ul laptopului "te-a lăsat" (nu se mai luminează) atunci n-ai decât să conectezi un monitor obişnuit.

Un document Web are de obicei trei componente care se pot dezvolta separat: HTML care serveşte pentru descrierea conţinutului propriu-zis (marcând paragrafele de text, titlurile de secţiuni, structurile tabelare sau liniare precum tabelele şi enumerările, imaginile, etc.); CSS care asociază stiluri diverselor elemente HTML; javaScript pentru a asocia anumitor elemente HTML posibilităţi de a reacţiona într-un anumit fel, într-un anumit context.

SF permite construirea de aplicaţii care produc documente Web, separând lucrurile conform arhitecturii Model-View-Controller, a cărei "filozofie" este excelent descrisă şi conturată concret în Exploring Symfony's Code.

Precizări elementare despre clase şi obiecte

Am fi vrut mai sus şi anumite lămuriri privind bazele lucrului cu clase şi cu obiecte în PHP… Iniţiem acum un mic experiment, creând pentru început fişierul test-clase conţinând codul următor:

#!/usr/bin/php
<?php

class TestClasa { }

$obiect = new TestClasa;
var_dump($obiect);

$alt_obiect = new TestClasa;
var_dump($alt_obiect);

Avem aici un script PHP pentru CLI; el trebuie făcut "executabil": chmod 0755 test-clase, după care poate fi invocat de pe linia de comandă a terminalului la fel cum lansam mai la început, ./symfony.

TestClasa este o clasă PHP şi anume una… vidă; dar şi aşa - vedem imediat că are o anumită utilitate.

new este un operator global - însuşit de altfel, de multe limbaje: C++, java, javaScript, etc. - care asigură crearea unor "instanţe" (sau "obiecte") ale clasei indicate; este suficient să ne amintim cum funcţionează new în C++:
int* ref_tablou = new int[100];
anume: new caută o zonă de memorie liberă având dimensiunea necesară stocării a 100 de valori de tip int şi returnează (în ref_tablou) adresa acestei zone.

test-clase creează două obiecte TestClasa, indicate de var_dump() prin #1 şi #2 şi care nu deţin nimic:

     vb@localhost:~/webschool/doc$ ./test-clase
     object(TestClasa)#1 (0) {}
     object(TestClasa)#2 (0) {}

Să modificăm acum scriptul test-clase astfel (omitem primele două linii - ele trebuie păstrate totdeauna):

class TestClasa { }

$obiect = new TestClasa;

$obiect -> myVar1 = "Valoare pentru proprietatea myVar1";
var_dump($obiect);

echo $obiect -> myVar1 . "\n";

Executând, vedem că acum $obiect (deşi instanţiat dintr-o clasă vidă) conţine un item exprimat prin "cheia" myVar1 şi valoarea asociată (sub forma "cheie" => "valoare"):

     vb@localhost:~/webschool/doc$ ./test-clase
     object(TestClasa)#1 (1) {
       ["myVar1"]=> string(34) "Valoare pentru proprietatea myVar1"
     }
     Valoare pentru proprietatea myVar1

$obiect->myVar1 = valoare permite adăugarea în obiectul respectiv (dar nu într-o variabilă oarecare, ci numai într-o instanţă de clasă PHP) a unei "proprietăţi" noi (precizând un nume "myVar1" şi o valoare). Mai general, -> este operatorul de acces la un membru al clasei (în alte limbaje avem în acest scop şi operatorul . dar în PHP, acesta desemnează concatenarea şirurilor şi operatorii nu pot fi "supraîncărcaţi").

Mai departe, vedem şi că proprietăţile adăugate unui obiect ţin strict de acesta, nu de clasa din care provine:

class TestClasa { }

$obiect = new TestClasa;

$obiect->myVar1 = "Valoare pentru proprietatea myVar1";
$obiect->myVar2 = get_class($obiect) . " (clasa din care s-a instanţiat obiectul)";  

echo "Obiectul creat deţine aceste proprietăţi:\n";
foreach (get_object_vars($obiect) as $nume => $valoare) {
    echo "\t" . $nume . " => " . $valoare . "\n";
}

echo get_class($obiect)." are ".count(get_class_vars('TestClasa'))." variabile\n";
     vb@localhost:~/webschool/doc$ ./test-clase
     Obiectul creat deţine aceste proprietăţi:
         myVar1 => Valoare pentru proprietatea myVar1
         myVar2 => TestClasa (clasa din care s-a instanţiat obiectul)
     Clasa TestClasa are 0 variabile

Am implicat mai sus câteva funcţii utilitare pentru clase şi obiecte. get_object_vars(obiect) furnizează un tablou care conţine proprietăţile obiectului (definite în clasa din care provine, sau adăugate lui ulterior instanţierii) şi i-am listat conţinutul folosind construcţia PHP foreach( $un_tablou as $nume => $valoare ).

Acum să definim o clasă mai consistentă:

class TestClasa { 
    private $data = array();

    public function set($name, $value) {
        $this->data[$name] = $value;
    }

    public function get($name) {
        return isset($this->data[$name]) ? $this->data[$name] : "$name nu există!\n";
    }
}

$obj1 = new TestClasa;
$obj1->set('myVar1', "Valoare myVar1");

echo "Obiect 1:\n"; var_dump($obj1);

echo "myVar1 => " . $obj1->get('myVar1');
echo "\n" . $obj1->get('myVar2');
     vb@localhost:~/webschool/doc$ ./test-clase
     Obiect 1:
     object(TestClasa)#1 (1) {
       ["data":"TestClasa":private] => array(1) {
                                         ["myVar1"]=> string(14) "Valoare myVar1"
       }   
     }
     myVar1 => Valoare myVar1
     myVar2 nu există

Obiectele clasei TestClasa vor deţine fiecare, câte un tablou data. În definiţia clasei, $data este calificat prin private - aşa că acest tablou nu va putea fi accesat direct din niciun obiect: echo count($obj1->data); de exemplu, ar provoca eroarea "Cannot access private property TestClasa::$data". Din acest motiv, sunt prevăzute metodele de acces - calificate public - set() şi get().

$obj1->set(nume, valoare) va permite ca în tabloul privat $obj1::data deţinut de obiectul existent $obj1 să se înscrie proprietatea respectivă, ca şi când s-ar face atribuirea $obj1->data[nume] = valoare; această atribuire directă nu este posibilă din $obj1, dar poate fi făcută din interiorul metodei set() - pentru că funcţiile membre ale clasei au acces inclusiv la membrii de tip private.

Ar trebui să observăm în cele din urmă, absurditatea situaţiei: vrei să accesezi din metoda set() internă clasei, un "membru" al clasei, anume "tabloul" $data… în realitate, acest tablou nu există în acel moment!
Clasa ca atare nu este decât un şablon după care se vor construi obiectele, un fel de cod genetic. "Membrii" declaraţi în codul clasei nu există de fapt, decât începând din momentul creării unui obiect de specia respectivă, fiinţând în cadrul acelui obiect, cât timp există acesta.

Soluţia acestei dileme - în toate limbajele OOP - a constat în constituirea unui "operator" special, desemnat de obicei prin this, care atunci când este utilizat în interiorul clasei referă instanţa curentă a clasei.

În obj1->set(...) metoda set() (care este comună obiectelor clasei) este invocată din obiectul $obj1, încât $this din $this->data care apare în interiorul acestei metode, referă $obj1 - şi astfel, va fi afectat tabloul data propriu obiectului $obj1 (şi numai pentru acest obiect).

Este mare diferenţă între $this->data şi $this->$data; în primul caz este vizat corect membrul $data, dar în al doilea ar fi vorba de un eventual membru al clasei care este referit prin variabila $data.

Să mai facem acum doar un singur lucru: să înlocuim în definiţia clasei, denumirile set şi get respectiv cu __set şi __get ("metode magice" în PHP, cum am precizat deja înainte). Atunci vom putea folosi o sintaxă mai comodă:

$obj1 = new TestClasa;
$obj1->myVar1 =  "Valoare myVar1";

echo "Obiect 1:\n"; var_dump($obj1);

echo "myVar1 => " . $obj1->myVar1;
echo "\n" . $obj1->myVar2;

În loc de $obj1->set('myVar1', "Valoare myVar1"); putem folosi acum $obj1->myVar1 = "Valoare myVar1"; şi analog pentru "get" (cu aceleaşi rezultate de execuţie ca în cazul anterior).

post scriptum: toate se leagă…

Am lucrat şi m-am legat mai bine de trei săptămâni la acest articol. S-au petrecut între timp atâtea - de la inundaţii de proporţii peste tot şi cutremurul din Chile, la dezvăluirile aberante de cum se fac averile şi cum acced persoane de calitate dubioasă pe postul de 'statul sunt eu'; dar desigur şi altele, mi-au captat atenţia.

Federer a pierdut dureros, cam fără joc, dar nu acel Federer…; Justine şi-a pierdut neasemuitul său rever, iar Şarapova nu-şi găseşte de loc serviciul. Ronnie O'Sullivan joacă iarăşi genial şi te întristezi că o lovitură lipsită de şansă i-a răsturnat meciul cu Selby, alt mare jucător şi acesta.

Am făcut vreo 100-150 de partide uşoare (de şah, desigur); am pierdut pe drept vreo 20; am cedat alte vreo 30, neîndurând soluţia partenerului, de a aşteapta de-acuma epuizarea timpului. E bine - nu mai sunt pretenţiile de pe vremuri, mă amestec şi eu printre jucătorii modeşti de care sunt pline serverele şi mă las încântat de câte un mesaj "good game" ori "you are a verry good player"…

S-a mai stricat în mijlocul lucrului (şi furtunii, probabil vinovate?) şi display-ul unuia dintre laptopurile mele; stai şi vezi apoi despre ce este vorba şi ce-i de făcut (backlight-failure, etc.).

Dar ceea ce chiar m-a tulburat foarte mult şi a decis aceste rânduri "în plus", este dispariţia unei persoane - şi chiar necunoscute mie (că muzica "uşoară" e un domeniu mai greu pentru mine).
Dar de fapt cum vine asta, "necunoscut"…?!

Ce greu este să te menţii şi să faci faţă în oricare sferă de valori, de la tenis şi snooker la muzica uşoară, şi oriunde…
Mai devreme sau mai târziu cedezi într-un fel sau în altul. Te poate învinge unul ajuns aproape ca tine de bun, sau te poate dărâma mediocritarea şi vulgul profitor din spaţiul comun; poate că şi într-un caz şi în celălalt e ca şi cum te-ai învinge tu însuţi, iar uneori… cum-necum, realizezi că e aşa de simplu s-o şi faci.

necunoscut… Ce să însemne decât neputinţă amară, târzie şi grea. Şi în voia compasiunii şi sincerităţii vreau să dedic acest articol conjunctural,

unei persoane al cărei zâmbet şi privire nu se pot uita

vezi Cărţile mele (de programare)

docerpro | Prev | Next