Sei sulla pagina 1di 13

Php e Microsoft Access a cura di Fabio Sutto Access in sintesi

scritto mercoled 18 settembre 2002

Una questione che di tanto in tanto si affaccia sul forum php riguarda la possibilit di interagire con Microsoft Access: anche se il binomio php-mysql rappresenta un duo probabilmente imbattibile nell' ambito WEB, chiunque operi con server Windows pu trovarsi ad avere a che fare con il popolare database desktop di Microsoft, e usando le dovute accortezze i risultati possono essere molto soddisfacenti. In realt ci che noi identifichiamo comunemente come "database Access" comprende tre componenti principali: un file *.mdb, il motore JET che opera dietro le quinte per la gestione del database (elabora le queries e tiene sotto controllo le tabelle), e l' interfaccia fornita dall'applicativo MS Office. L'ultimo componente scindibile, tanto che nulla ci impedisce di interagire direttamente con JET attraverso Vbscript, Visual Basic o magari proprio Php. Access appartiene alla categoria dei DBMS (Database Management System) desktop che viene contrapposta a quella dei DBMS client/server della quale, ad esempio, fanno parte Mysql e Sql Server 2000. Un Server di database fa quello che dice il suo nome: un robusto sistema di gestione che rimane in esecuzione permanente in background, supervisiona gli accessi e inoltra le richieste ai database che si trovano sotto il suo controllo. Anche i file di Access possono essere condivisi tra pi utenti, ma non c' alcuna interfaccia che controlli gli accessi e risponda alle richieste. In maniera molto sbrigativa possiamo dire che un DBMS desktop si mostra efficiente soprattutto nell'ambito della macchina in cui si trova e non a livello di rete, infatti quando lo utilizziamo come database di appoggio per un sito WEB il file *.mdb di Access solitamente si trova in una sottodirectory protetta dagli accessi esterni, ed i nostri script si collegano ad esso tramite un percorso fisico. Sembra che il futuro di Access stia nel diventare l'interfaccia privilegiata per SQL Server (con il rilascio di Access 2002 non c' stato un aggiornamento rispetto alla versione 4.0 del motore JET), nonostante tutto JET avr ancora vita lunga poich presenta anche caratteristiche non trascurabili: un buon database di sola lettura che archivia dati e struttura in un unico file *.mdb facilmente trasferibile, inoltre pur essendo perfettamente in grado di sostenere un sito di medie dimensioni molto pi semplice da usare di un DBMS client/server. Il modo migliore per operare con Access attraverso Php ricorrere alle apposite funzioni ODBC (Open Database Connectivity, l'API universale di Microsoft), questo per significa che dobbiamo rendere disponibile il database per ODBC creando un DSN di sistema (Data Source Name), ossia un file speciale che contiene informazioni relative al percorso e al tipo di database. Per nostra sfortuna molti fornitori di hosting non rendono disponibile un DSN per il database e consentono soltanto connessioni senza DSN, e questa una delle situazioni in cui le funzioni COM di Php sono di grande aiuto. Discutine sul forum di Html.it. COM, pensare per oggetti Chi programma in ambiente Windows ha a disposizione diverse architetture per l'Accesso ai dati che spesso si sovrappongono e sono in grado di effettuare le stesse operazioni, operando ad un livello pi o meno basso. L'acronimo COM sta per Component Object Model, fu introdotto da Microsoft con lo scopo di mettere a disposizione un modo semplice e universale per instaurare una comunicazione tra i

vari moduli dei programmi per Windows. L'argomento piuttosto complesso e questo articolo rischierebbe di trasformarsi in un piccolo trattato di programmazione in ambiente Windows se ci occupassimo di concetti quali COM, ODBC, OLEDB, DAO, ADO quindi per eventuali approfondimenti vi invito a fare riferimento alla relativa documentazione Microsoft. Per i nostri scopi sufficiente sapere che le funzioni COM sono disponibili soltanto nella versione WIN di Php, e grazie ad esse avremo l'impressione di programmare in ASP utilizzando la sintassi Php (anzich i linguaggi propri di quell'ambiente, Vbscript e Javascript). Questo significa che dovremo abituarci alla sintassi Object Oriented e ad utilizzare le classi "prefabbricate" e gli oggetti messi a disposizione in ASP un po' come facciamo abitualmente con le funzioni predefinite di Php: ovvero limitandoci a capire cosa fanno e utilizzando a nostro vantaggio propriet e metodi senza porci troppe domande sul loro funzionamento interno. Ci comporter anche che sar pi utile possedere gi una qualche esperienza con gli oggetti ADO e il JET-SQL piuttosto che una conoscenza approfondita di Php. ADO, ActiveX Data Objects E' un'architettura che fornisce oggetti di alto livello per l'accesso ai dati, e in particolare contiene 6 oggetti per la comunicazione con i database. Qui ci limiteremo ad elencare sinteticamente i 3 principali: 1) l'oggetto Connection consente di stabilire la connessione. 2) l'oggetto Recordset consente di operare con i dati contenuti in una tabella e recuperati attraverso una query. 3) l'oggetto Field una collection (una sorta di array) che rappresenta le colonne di una tabella JET-SQL E' il "dialetto" Access dello Structured Query Language, si discosta solo in parte dallo standard (che comunque nessun DBMS supporta interamente). Consente di operare con JET effettuando quasi tutte le operazioni possibili attraverso i soli metodi degli oggetti ADO: possibile creare stored procedures, proteggere il database con password etc. etc. Probabilmente l'unica cosa che non in grado di fare creare un file di database dal nulla (pi avanti vedremo come utilizzare ADOX, un estensione di ADO, a questo scopo). Nell'articolo ho tralasciato volutamente una descrizione della sintassi JET-SQL, in parte perch quasi sempre comprensibile per chi ha confidenza con lo standard SQL, e un po' perch ai seguenti links possibile trovare degli articoli molto esaurienti (informazioni in lingua inglese): JET-SQL: nozioni di base Intermediate JET-SQL Caratteristiche avanzate di JET-SQL Dopo questa lunga ma, spero, utile premessa finalmente giunto il momento di mettere le mani su un po' di codice: eseguite gli esempi nell'ordine in cui vengono descritti, e se volete testate di tanto in tanto i risultati aprendo il file mytest.mdb con Access (l'applicativo office). Tutti gli script sono stati testati in Windows 2000, sia con Apache che con IIS. La stringa di connessione Quando non possibile impostare un DSN, le informazioni necessarie per la connessione al database devono essere inserite all'interno di una stringa, che poi verr passata come argomento al metodo Open() dell'oggetto predefinito ADODB.Connection.

Esempio di stringa di Inizializzazione/Connessione Deve contenere almeno due elementi: 1) l'indicazione del motore di database, in questo caso JET/Access $cn_string="Provider=Microsoft.Jet.OLEDB.4.0;" ;

2) l'origine dati, cio il percorso del database. Bisogner fornire sempre il percorso assoluto, qui di seguito nel formato richiesto se si esegue php con Apache, mentre nel caso in cui ci dovessimo appoggiare ad IIS (il webserver Microsoft) gli slash ("/") potanno essere sostituiti da backslash ("\") $cn_string.="Data Source=C:/www/prova/PhpAccess/mytestdb.mdb;" ;

La stringa di connessione pu contenere anche altre informazioni a seconda delle operazioni che si vogliono effettuare, ne presentiamo alcune qui di seguito Mode Indica la modalit di apertura del database, quando non viene specificata l'apertura in lettura/scrittura condivisa (costante ADO adModeReadWrite, valore 3). Nell'esempio l'apertura in modalit esclusiva (costante ADO adModeShareExclusive, valore 12) che necessaria quando ad esempio si desidera modificare la password condivisa del database. $cn_string.="Mode= 12 ;"

Engine type E' utile solo al momento della creazione del database indica la versione del motore JET che utilizzeremo, se il database esiste gi ininfluente per la connessione. Il codice numerico 5 corrisponde a JET 4.0 (Access 2000-2002). $cn_string.="Jet OLEDB:Engine Type= 5;"

Password (protezione condivisa) Se il database protetto da una password unica uguale per tutti (livello di protezione condiviso) dobbiamo impostarla qui al momento della connessione $cn_string.="Jet OLEDB:Database Password= apritisesamo;"

Utenti (protezione a livello utenti) Se il database protetto a livello utenti (ognuno con la propria password ed i propri permessi) dobbiamo indicarli in questo modo $cn_string.="User ID=user; Password=password;"

Creare un database sul server Ovviamente nulla impedisce di creare il database attraverso l'interfaccia Office e poi fare l'upload del file .mdb ma, visto che c' modo di fare tutto attraverso il codice, ci parso interessante illustrare anche questa possibilit. <?php /**** Creiamo delle costanti che definiscono il tipo di motore di database JET, l'ultima versione, JET 4.0, corrisponde ad Access 2000 e 2002 e viene indicata con il codice numerico 5 ****/ define("JET10", 1) ; define("JET11", 2) ; define("JET20", 3) ; define("JET3x", 4) ; define("JET4x", 5) ; /**** Inserisci qui il percorso dove vuoi che il database venga creato, nell'esempio la stessa directory dello script, il formato quello richiesto da Apache ****/ $path= "C:/www/prova/PhpAccess/" ; /**** Identifichiamo il database ****/ $db_name= "mytest.mdb" ; $dsource=$path.$db_name ; /**** La stringa di inizializzazione/connessione ****/ $cn_string="Provider=Microsoft.Jet.OLEDB.4.0;" ; $cn_string.="Jet OLEDB:Engine Type=".JET4x.";" ; $cn_string.="Data Source=$dsource;" ; /**** Creo un'istanza dell'oggetto predefinito ADOX.Catalog attraverso COM (ADOX un'estensione di ADO) ****/ if ( file_exists($dsource) ){ die("Il database esiste gi") ; } $db= new COM("ADOX.catalog") or die("Impossibile istanziare ADOX") ; /**** Creo il database tramite il metodo Create dell'oggetto ADOX.Catalog ****/ $db->Create($cn_string) or die("Impossibile creare db") ; /**** Liberiamo la memoria ****/ $db->Release() ;

$db= null ; ?> La connessione al database Ora che il database stato creato, vediamo come connetterci ad esso sempre attraverso COM e ADO <?php /**** Inserisci qui il percorso dove si trova il database, ****/ $path= "C:/www/prova/PhpAccess/" ; /**** identifichiamo il database ****/ $db_name= "mytest.mdb" ; $dsource=$path.$db_name ; /**** Come sempre la stringa di connessione ****/ $cn_string="Provider=Microsoft.Jet.OLEDB.4.0;" ; $cn_string.="Data Source=$dsource;" ; /**** Istanzio un oggetto Connection e apro la connessione con il database atraverso il metodo Open() dell'oggetto. Il metodo prende come argomento la stringa di connessione, oppure il DSN quando c'. ****/ if (!file_exists($dsource) ){ die("Il database non esiste") ; } $cn= new COM("ADODB.Connection"); $cn->open($cn_string) ; // -- CODICE --//Interrogo/modifico il DB // --- CODICE--/**** Chiudo la connessione e libero la memoria ****/ $cn->Close() ; $cn->Release() ; $cn= null ; ?>

Interrogare e modificare il database Dopo la connessione possiamo inviare dei comandi al database, per fare ci dovremo istanziare un oggetto RECORDSET (pi avanti vedremo meglio cosa esso sia) e passare la richiesta al suo metodo Open(). Creare una tabella Aggiungiamo una tabella al database con SQL (ma non l'unico modo) <?php /* Definiamo semplice tabella di esempio con tre campi: COUNTER corrisponde ad un campo numerico AUTOINCREMENT di mysql. Le parentesi quadrate sono obbligatorie solo se il nome del campo contiene degli spazi */ $query="CREATE TABLE Test_Table ( [id] COUNTER NOT NULL, [Nome] text(15) NOT NULL, [Cognome] text(15) NOT NULL, PRIMARY KEY ([id]) );" ; /* I parametri di connessione */ $path= "C:/www/prova/PhpAccess/" ; $db_name= "mytest.mdb" ; $dsource=$path.$db_name ; $cn_string="Provider=Microsoft.Jet.OLEDB.4.0;" ; $cn_string.="Data Source=$dsource;" ; /* La connessione */ if (!file_exists($dsource) ){ die("Il database non esiste") ; } $cn= new COM("ADODB.Connection"); $cn->open($cn_string) ; /* Istanziamo un oggetto Recordset e inviamo la query attraverso il metodo Open() */ $rs= new COM("ADODB.Recordset") ; $rs->Open($query,$cn) ; /* Pulizia dell'oggetto Recordset */ $rs->Release() ; $rs= null ; /* Chiudo la connessione e libero la memoria */ $cn->Close() ; $cn->Release() ; $cn= null ; ?> Inserire record

Ecco una query JetSQL molto semplice che ci consentir di inserire alcuni valori nella nostra tabella di prova $query="insert into Test_Table (nome,cognome) values ('Mario','Rossi')" ;

A differenza della sintassi SQL/Mysql per far s che il campo COUNTER incrementi automaticamente il suo valore, non possiamo inserire NULL ma dobbiamo limitarci ad ignorare la presenza del campo. Sostituite questa query a quella dell'esempio precedente ed eseguite lo script per due o tre volte (inserendo alcuni record contenenti nomi diversi) per proseguire con gli esempi successivi. Recuperare valori <?php /* La query SQL, le parentesi quadrate sono necessarie solo quando i nomi dei campi presentano spazi */ $query="select [nome],[cognome] from Test_Table" ; /* I parametri di connessione */ $path= "C:/www/prova/PhpAccess/" ; $db_name= "mytest.mdb" ; $dsource=$path.$db_name ; $cn_string="Provider=Microsoft.Jet.OLEDB.4.0;" ; $cn_string.="Data Source=$dsource;" ; /* La connessione */ if (!file_exists($dsource) ){ die("Il database non esiste") ; } $cn= new COM("ADODB.Connection"); $cn->open($cn_string) ; /* Istanziamo un oggetto Recordset e inviamo la query attraverso il metodo Open() */ $rs= new COM("ADODB.Recordset") ; $rs->Open($query,$cn) ; /* Ciclo per recuperare i valori dal recordset EOF= tutto il set di dati stato esaminato e il cursore giunto in fondo */ while(!$rs->EOF){ echo($rs->Fields['nome']->value." ".$rs->Fields['cognome']->value." ") ; $rs->MoveNext() ; } /* Chiusura Recordset (da non farsi nelle query di comando) */ $rs->Close() ; /* Pulizia dell'oggetto Recordset */

$rs->Release() ; $rs= null ; /* Chiudo la connessione e libero la memoria */ $cn->Close() ; $cn->Release() ; $cn= null ; ?> L'oggetto Recordset E' giunto il momento di compiere una breve analisi dell'oggetto RECORDSET con cui abbia mo lavorato fino ad ora. Come avrete notato eseguire una query select attraverso il metodo Open() dell'oggetto Recordset non molto differente dall' eseguire in Mysql: $risultato=mysql_query($query) ; e poi scorrere i dati contenuti in $risultato in questo modo, while ($row = mysql_fetch_object($risultato)) { echo $row->user_id; echo $row->fullname; }

L'esecuzione della query non fa altro che restituirci un set di dati le cui righe sono array che corrispondono ai record, mentre gli elementi di ogni array corrispondono al contenuto dei campi dei record stessi. Tuttavia il metodo Open() un po' pi complicato e pu contenere altri parametri facoltativi oltre alla query e all'identificativo della connessione, questa la forma completa $rs->Open($comando,$id_connessione,$cursor_type,$lock_type,$tipo_comando) ;

tipo_comando specifica la modalit di richiesta fatta al database, la richiesta pu avere forme diverse: il nome di una tabella della quale vogliamo visualizzare tutti i records, una stringa SQL o anche l'identificativo di una stored procedure memorizzata in precedenza nel database. Normalmente JET in grado di determinare da solo questo parametro, ma per motivi di efficienza nelle prestazioni preferibile inserire l'indicazione esplicitamente. Nei nostri esempi abbiamo privilegiato l'invio di comandi attraverso query SQL per diverse ragioni che vedremo tra poco. Il valore da fornire al parametro tipo_comando qualora si passi una query SQL 1 (corrispondente alla costante VB adCmdText), una lista di tutti i comandi possibili reperibile qui. cursor_type determina il modo in cui possiamo scorrere il recordset: il cursore di default (che il pi efficiente in termini di prestazioni) consente di scorrere il set di risultati soltanto dall'alto al basso (forward-only, valore 0) e soltanto una volta. Un cursore diverso (detto "scorrevole", valore 1) permette di definire in anticipo quante righe di risultati la query ha restituito (attraverso il metodo $rs->RecordCount()), tuttavia di solito la cosa davvero importante capire se vi siano stati risultati oppure no, e questo possiamo determinarlo anche se operiamo con un cursore forward-only:

sar sufficiente verificare se appena aperto il recordset si trova gi EOF, come nell' esempio seguente /* Ciclo per recuperare i valori dal recordset */ //SE il cursore NON gi EOF ..... if(!$rs->EOF){ while(!$rs->EOF){ echo($rs->Fields['nome']->value." ".$rs->Fields['cognome']->value." ") ; $rs->MoveNext() ; } } //.... ALTRIMENTI else{ echo("La query non ha prodotto alcun risultato") ; }

lock_type rappresenta il bloccaggio del recordset che di default "sola lettura", inviare un comando SQL l'unico modo di effettuare modifiche al database pur aprendo in modalit "sola lettura": quindi le operazioni di update attraverso SQL sono atomiche e non espongono ai danni di possibili accessi concomitanti. Ricordiamo, solo per completezza, che anche possibile creare implicitamente una connessione aprendo direttamente un recordset in questo modo $rs= new COM("ADODB.Recordset") ; $rs->Open($query, $cn_string) ;

Chi vorr approfondire le propriet dell'oggetto recordset trover tutte le risposte nella MSDN library , in particolare chi scrive ha trovato molto interessante l'utilizzo delle stored procedures. Dare una password al database Impareremo come proteggere a livello condiviso (password unica per tutti), abbiamo gi accennato alla cosa quando si parlato delle propriet opzionali della stringa di connessione. Per prima cosa dovremo connetterci al database in modalit esclusiva, pertanto setteremo la propriet MODE della stringa di connessione a 12, il valore di default 3 (apertura condivisa in lettura/scrittura) e trovate qui una lista completa delle possibili modalit di connessione. <?php /* Il percorso fisico del database */ $path= "C:/www/prova/PhpAccess/" ; /* identifichiamo il database */ $db_name= "mytest.mdb" ; $dsource=$path.$db_name ; /* Le password che vogliamo scambiare sostituisci quelle che preferisci tu */ $old_pw="password_attuale"; $new_pw="nuova_password" ; /* La query */ $query="ALTER DATABASE

PASSWORD $new_pw $old_pw ; " ; /* La stringa di connessione, notare MODE */ $cn_string="Provider=Microsoft.Jet.OLEDB.4.0;" ; $cn_string.="Mode= 12 ;" ; $cn_string.="Jet OLEDB:Database Password=$old_pw;" ; $cn_string.="Data Source=$dsource;" ; if(!file_exists($dsource)){ die("Il database non esiste, connessione fallita") ; } $cn= new COM("ADODB.Connection"); $cn->Open($cn_string) ; $rs=new COM("ADODB.Recordset") ; $rs->Open($query,$cn) ; /* Pulizia del recordset */ $rs->Release() ; $rs= null ; /* Chiudiamo la connessione e liberiamo la memoria */ $cn->Close() ; $cn->Release() ; $cn= null ; ?>

Se inseriamo la password per la prima volta, la variabile $old_pw dovr avere valore NULL (che poi non un valore), mentre se desideriamo togliere la protezione dal database dovremo dare valore NULL a $new_pw. Ovviamente la stringa di connessione dovr comprendere l'apposito parametro relativo alla password (vedi propriet della ConnString nelle pagine precedenti). Impostare una protezione al livello utenti leggermente pi complicato e richiederebbe una spiegazione piuttosto lunga, pertanto vi rinvio ancora una volta alla MSDN LIBRARY (potete fare una ricerca con parole chiave "jet database user-level security"). Compattare il database Le operazioni di scrittura, cancellazione e riscrittura comportano un aumento progressivo delle dimensioni del database, effettuando periodicamente una compattazione otterremo un' ottimizzazione delle prestazioni oltre che un risparmio di spazio sul disco. Questa operazione una di quelle che non si possono fare attraverso JET-SQL, la illustriamo anche perch si tratta di un metodo alternativo per impostare una protezione, a livello utenti o a livello condiviso, al database. <?php /* Inserisci qui il percorso dove si trova il database, */ $path= "C:/www/prova/PhpAccess/" ;

/* Nome e password del database da compattare */ $olddb="mytest.mdb" ; $oldpass="" ; /* Nome provvisorio database compattato */ $newdb="new_mytest.mdb" ; $newpass="mypassword" ; /* ConnString verso il database da compattare */ $oldConn="Provider=Microsoft.Jet.OLEDB.4.0;" ; $oldConn.="Data Source=".$path.$olddb.";" ; $oldConn.="Jet OLEDB:Database Password=$oldpass ;" ; /* ConnString verso il nuovo database (compattato) */ $newConn="Provider=Microsoft.Jet.OLEDB.4.0;"; $newConn.="Data Source=".$path.$newdb.";" ; $newConn.="Jet OLEDB:Database Password=$newpass ;" ; /* Controllo che non esista gi un file con il nome provvisorio scelto */ if(file_exists($path.$newdb)){ die("Esiste gi un file con il nome provvisorio che hai scelto") ; } /* Istanzio l'oggetto che fornisce il metodo Compact() */ $je=new COM("JRO.JetEngine") or die("Compact fallito"); /* Compatta il database e setta una nuova password */ $je->CompactDatabase($oldConn,$newConn) ; $je->Release() ; $je= null ; /* Elimina il vecchio database */ unlink($olddb); /* Rinomina il db compattato con il vecchio nome */ rename($newdb,$olddb) ; ?> Ricavare informazioni sulla struttura del database A questo scopo l'oggetto Connection mette a disposizione il metodo OpenSchema(): se si desiderano informazioni sul numero e sul nome delle tabelle contenute nel database dovremo passare come argomento di OpenSchema il codice numerico 20, mentre per ricevere informazioni sui campi di una singola tabella il codice 4. Quando apriamo il recordset con OpenSchema(20) dovremo filtrare il tipo di tabella che cerchiamo, JET infatti contiene delle tabelle per la sua gestione interna che non ci interessano: noi dovremo cercare le tabelle di tipo "TABLE". Per un elenco completo delle informazioni reperibili attraverso OpenSchema potete visitare questa pagina. I normali parametri di connessione

<?php /* Il percorso fisico del database, inserisci il tuo */ $path= "C:/www/prova/PhpAccess/" ; /* identifichiamo il database */ $db_name= "mytest.mdb" ; $dsource=$path.$db_name ; /* La stringa di connessione */ $cn_string="Provider=Microsoft.Jet.OLEDB.4.0;" ; $cn_string.="Data Source=$dsource;" ; /* La connessione */ $cn= new COM("ADODB.Connection"); $cn->open($cn_string) ; ?>

Informazioni sulle tabelle

<?php /* OpenSchema(20) Elenca le tabelle, cerchiamo le tabelle di tipo TABLE */ $rs=$cn->openSchema(20) ; while(!$rs->EOF){ if(trim($rs->Fields["TABLE_TYPE"]->value=="TABLE")){ echo($rs->Fields["TABLE_NAME"]->value." ") ; } $rs->movenext() ; } $rs->close() ; $rs = null; ?>

Informazioni sui campi di una tabella specifica <?php /* La tabella che vogliamo esaminare */ $nome_tabella="Test_Table" ; /* OpenSchema(4) Elenca i campi per ogni tabella */ $rs=$cn->openSchema(4) ;

while(!$rs->EOF){ if(trim($rs->Fields["TABLE_NAME"]->value==$nome_tabella)){ echo($rs->Fields["COLUMN_NAME"]->value) ; echo(" ** Tipo di dato: ".$rs->Fields['DATA_TYPE']->value." ") ; } $rs->movenext() ; } $rs->Close() ; $rs->Release() ; $rs = null; $cn->Close(); $cn->Release() ; $cn=null ; ?> Conclusioni Nonostante questa sia probabilmente una delle guide pi dettagliate che si possano trovare su la coppia Access/Php, l'argomento non pu certo dirsi esaurito. Da un lato ho cercato di fornire degli spunti a 360 gradi che, per certi aspetti, possono dirsi di livello avanzato anche per chi programma abitualmente in ambiente ASP, dall'altro per favorire la leggibilit il codice stato presentato in maniera lineare evitando di racchiuderlo in funzioni (come invece sarebbe pratica corretta fare): in ogni caso chi decidesse, per scelta o per necessit, di operare con Access e Php insieme non potr prescindere da una comprensione degli oggetti ADO. Fortunatamente una ricerca nella MSDN LIBRARY di solito consente di chiarire il 99% dei nostri dubbi. Ovviamente per dell'ottimo materiale in italiano potete visitare freeasp.html.it :)

Potrebbero piacerti anche