mercoledì 29 agosto 2007

Joomla 1.5: Sviluppo Componenti (2)

Arrivati a questo punto si è visto come utilizzare l'oggetto View ed il Template ad esso associato per far visualizzare al componente di esempio (MyForm) un modulo di immissione dati la cui struttura abbiamo definito nell'oggetto Model.

Il passo successivo sarà scrivere il codice necessario per acquisire i dati inseriti dall'utente e salvarli in una tabella del database.

Il risultato finale di tutto quanto diremo è nel file myform2.zip. Consiglierei di scaricarlo subito e scompattarlo in una cartella del vostro hard disk perché durante la spiegazione salteremo da un file all'altro ed averli tutti subito a portata di mano incrementerà le probabilità di capirci qualcosa!

Creazione Tabella nel database

La tabella dove salvare i dati dovrà essere creata quando il componente viene installato e rimossa quando viene disinstallato. Si devono quindi scrivere due distinti script sql.

install.sql

DROP TABLE IF EXISTS `#__myform`;

CREATE TABLE `#__myform` (
  `id` int(11) NOT NULL auto_increment,
  `nome` varchar(25) NULL,
  `cognome` varchar(25) NULL,
  `telefono` varchar(15) NULL,
  `indirizzo` varchar(120) NULL,
  `note` text NULL,
  `data` datetime NULL,
  PRIMARY KEY  (`id`)
) ENGINE=MyISAM AUTO_INCREMENT=0 DEFAULT CHARSET=utf8;

uninstall.sql

  DROP TABLE IF EXISTS `#__myform`;

Prima di eseguire la query i caratteri #__ vengono automaticamente sostituiti con il prefisso tabelle specificato in fase di installazione di Joomla. Se non sono state modificate le impostazioni predefinite il nome della tabella nel database sarà jos_myform.

Classe Table del componente

Oltre a creare la tabella nel database bisogna aggiungere al componente una classe specifica che fornisca al framework di Joomla le informazioni necessarie a compiere le operazioni sui dati nella tabella.

Per fare questo bisogna aggiungere un altro file al componente

File myform.php

in .../administrator/components/com_myform/tables/
class TableMyForm extends JTable {
  var $id = null;
  var $nome = null;
  var $cognome = null;
  var $telefono = null;
  var $indirizzo = null;
  var $note = null;
  var $data = null;

Una variabile di classe per ogni campo della tabella sul database con lo stesso nome del campo. Niente di più semplice!

  function __construct(& $db) {
    parent::__construct('#__myform', 'id', $db);
  }
} //Fine class TableMyForm

Il nome della tabella e del campo chiave primaria (id) sono necessari alle funzioni del framework per gestire le operazioni sui dati della tabella (inserimento, modifica, cancellazione).

File myform.xml

in .../administrator/components/com_myform/

Il file xml del componente dovrà essere aggiornato per riflettere le modifiche che abbiamo appena apportato.

1)
...
</files>
<install>
  <sql>
    <file charset="utf8" driver="mysql">install.sql</file>
  </sql>
</install>
<uninstall>
  <sql>
    <file charset="utf8" driver="mysql">uninstall.sql</file>
  </sql>
</uninstall>
...

I tag <install> e <uninstall>, com'è facile intuire, servono ad indicare quale dei due script deve essere eseguito in fase di installazione e quale in fase di disinstallazione del componente.

Gli attributi del tag <file> charset e driver in questa versione di Joomla vanno sempre impostati rispettivamente come utf-8 e mysql.

2)
 
...
<administration>
  <menu>My Form</menu>
  <files folder="admin">
    <filename>admin.myform.php</filename>
    <filename>tables/myform.php</filename>
    <filename>install.sql</filename>
    <filename>uninstall.sql</filename>
  </files>  
</administration>
...

Poiché i tag <filename> sono collocati tra <administration> e </administration> i due script e il file contenente la definizione della classe TableMyForm saranno installati in .../administrator/components/com_myform/

L'attributo folder del tag <files> indica che tutti questi files si trovano nella cartella admin del pacchetto di installazione.

Processare i dati ricevuti dal modulo

Facciamo un passo indietro. Se avete installato la versione preliminare del componente e creato un link ad esso secondo le istruzioni contenute nel post precedente, fate click sul link per visualizzare la pagina con il modulo di immissione dati. Con l'opzione Visualizza Sorgente Pagina del vostro browser controllate il codice html generato per il modulo.

Troverete

<form name="myform" method="post" action="index.php">
...
<input type="hidden" name="option" value="com_myform" />
<input type="hidden" name="task" value="submit" />

Vale la pena ricordare che questo codice è generato dal Template (in .../components/com_myform/views/myform/tmpl/default.php)

Le variabili passate attraverso i due campi hidden sono essenziali.

  • option comunica al framework di passare il controllo al file principale del componente com_myform
  • task indica l'azione che deve essere eseguita.

Il file principale del nostro componente è .../components/com_myform/myform.php e contiene, come abbiamo già visto, le due istruzioni seguenti

$controller = new MyFormController();
$controller->execute(JRequest::getVar('task'));

Il metodo execute() del Controller (implementato nella classe base JController) riceve come parametro il valore di task letto in questo caso da $_POST (method="post" nel tag di apertura <form>)

Il Controller esegue il metodo corrispondente al valore di task, nel nostro caso submit.

Non resta che implementarlo in .../components/com_myform/controller.php

class MyFormController extends JController {
 
  function submit() {
    $data = JRequest::getVar('myform', array(), 'post', 'array');
    $model =& $this->getModel('myform');
  
    if ($model->store($data)) {
      $this->setRedirect(
        JRoute::_('index.php?option=com_myform', false), 
        JText::_('Modulo Inviato con Successo')
      );
    } else {
      $view =& $this->getView('myform', 'html');
      $view->setModel($model);
      $view->assignRef('data', $data);
      $view->display();    
    }
  }
}

Per prima cosa la funzione JRequest::getVar legge da $_POST l'array myform. Tornando al sorgente html del modulo, poiché abbiamo

<input type="text" name="myform[nome]" id="nome" size="25" />

$data['nome'] conterrà il valore immesso nel campo nome. Stessa cosa per tutti gli altri campi.

Poi, ottenuto un riferimento all'oggetto Model se ne invoca il metodo store() passando come parametro l'array di dati da memorizzare nella tabella del database.

Se l'operazione ha avuto successo si imposta un reindirizzamento alla pagina principale del componente (setRedirect()).

In caso di errore il controllo torna a View perché il modulo deve essere mostrato nuovamente. Si utilizza setModel() per passare all'oggetto View un riferimento all'oggetto Model. La struttura dati che definisce la struttura del modulo è in Model quindi View deve poter accedere a Model.

Per consentire all'utente di correggere l'errore senza perdere tutti i dati già inseriti, si passa a View (con assignRef()) un riferimento all'array $data che verrà utilizzato per reimpostare il valore dei campi del modulo.

Il codice che fa questo è in .../components/com_myform/views/myform/tmpl/default.php

Infine si esegue la visualizzazione invocando il metodo display() di View.

Scriviamo il codice per il metodo store() di Model in .../components/com_myform/models/myform.php

class MyFormModelMyForm extends JModel {
  ...
  function store($data) {
    $row =& $this->getTable();
    if (!$row->bind($data)) {
      JError::raiseWarning(500, $row->getError());
      return false;
    }
    if (!$row->check()) {
      JError::raiseWarning(500, $row->getError());
      return false;
    }
    if (!$row->store()) {
      JError::raiseWarning(500, $row->getError());
      return false;
    }
    return true
  } 
}

Si ottiene per prima cosa un riferimento all'oggetto Table del nostro componente(classe TableMyForm)

Il metodo bind() della classe JTable (la classe base da cui abbiamo derivato TableMyForm) fa una cosa semplicissima. Legge campo per campo l'array $data e quando trova una corrispondenza tra una chiave dell'array e il nome di una variabile di classe di TableMyForm, assegna alla variabile di classe il valore corrispondente nell'array.

Quindi se abbiamo

$data['nome'] = 'Pippo'

Dopo l'esecuzione del metodo bind avremo

$row->nome = 'Pippo'

Il metodo bind() ritorna false in caso di errore. In questo caso si esce dalla funzione impostando una condizione di errore. Di JError e la gestione degli errori in Joomla si parlerà un'altra volta.

Il metodo check() di JTable è il metodo standard utilizzato per i controlli di validità sui dati prima di procedere al salvataggio degli stessi sul database. Lo implementeremo in seguito nella nostra classe TableMyForm.

Il metodo store() di JTable salva i dati nel database. Non dobbiamo scrivere nessuna istruzione sql. Una volta creata una istanza della nostra classe TableMyForm, e valorizzatene opportunamente i campi con il metodo bind(), le funzioni del framework hanno tutte le informazioni necessarie per creare ed eseguire la query sql.

Nota a margine. Le operazioni sul database eseguite dal componente di esempio sono solo quelle di inserimento: ogni volta che il modulo è inviato con successo un nuovo record è creato nel database. Non ci sono funzioni di modifica di un record esistente. Ma se ci fossero (e su questo per ora vi tocca fidarvi sulla parola), potremmo gestirle senza modifiche al codice del metodo store() dell'oggetto Model (MyFormModelMyForm).

Questo perché il metodo store() di JTable (occhio! Stesso nome del metodo, ma classi diverse) gestisce sia le operazioni di inserimento che quelle di modifica di un record.

E come fa a sapere quando eseguire l'una e quando l'altra?

Quando abbiamo creato la classe TableMyForm abbiamo indicato un campo (id) come chiave primaria.

function __construct(& $db) {
  parent::__construct('#__myform', 'id', $db);
}

Il metodo store() inserisce un nuovo record se la variabile di classe id in TableMyForm è null (o zero), altrimenti modifica il record con chiave corrispondente al valore di tale variabile.

Poiché non abbiamo un campo di nome id nel modulo di immissione dati, quando viene eseguito il metodo $row->bind() il valore di $row->id resta null, di conseguenza $row->store() crea sempre un nuovo record.

Validazione dati immessi nel modulo

Se lasciassimo il tutto così, cosa che sarei tentato di fare visto che mi sono dilungato in maniera mostruosa, il componente non effettuerebbe nessun controllo di validità sui dati immessi dall'utente. Quello che si inserisce nel modulo viene salvato nel database.

Il bello è che se si preme Invia lasciando il modulo vuoto, nel database viene creato un record nuovo con tutti i campi vuoti.

Direi che sia il caso di ovviare almeno a quest'ultimo inconveniente, lasciando come esercizio al lettore (la solita scusa quando uno non ha più voglia di scrivere!) l'implementazione di controlli più sofisticati.

Stabiliamo che il campo Nome e Cognome debbano essere obbligatoriamente inseriti. Si è già accennato che i controlli di validità sui dati si effettuano con il metodo check() dell'oggetto Table.

Abbiamo già inserito la chiamata al metodo nel codice dell'oggetto Model (in store()), però se non lo implementiamo in TableMyForm sarà utilizzato il metodo corrispondente della classe base JTable che si limita a ritornare sempre true, in pratica il controllo di validità darebbe sempre esito positivo.

Quindi inseriamo in TableMyForm in .../administrator/components/com_myform/tables/myform.php

class TableMyForm extends JTable {
...
  function check() {
    if (trim($this->nome) == '') {
      $this->setError (JText::_('Inserire il Nome.'));
      return false;
    }
    if (trim($this->cognome) == '') {
      $this->setError(JText::_('Inserire il Cognome.'));
      return false;
    }
    return true;
  }
} //Fine class TableMyForm

Non c'è molto da dire. Per effetto del metodo bind(), $this->nome e $this->cognome contengono i valori immessi dall'utente nei campi corrispondenti del modulo. Se anche uno solo è vuoto si imposta un messaggio di errore e si esce con false.

E' tutto per ora. Il componente può essere scaricato dal sito (myform2.zip) e installato. Se avete installato la versione precedente disinstallatela.

Potete provare a inserire dati nel modulo e inviarlo, ma ancora mancano le funzioni di visualizzazione dei dati per cui dovrete usare phpMyAdmin (o un altro strumento per l'amministrazione di MySql) per verificare che quanto inserito nel modulo sia salvato nella tabella.

La prossima volta completeremo il componente aggiungendo le funzioni di amministrazione.

7 commenti:

lamefarmer ha detto...

problema: ho una installazione Joomla 1.5 su XP con PHP 5.0.5 e IIs, e con chronoforms il CMS si comporta benissimo, mentre con i tuoi esempi se non commento la riga:
$controller->redirect();
succede che la pagina va in loop e inserisce all'infinito record in tabella (da sola). Dato che riderect viene usato con successo d'appertutto, non ho idea da cosa possa dipendere ... forse IIs ?

lamefarmer ha detto...

Precisazioni: andrebbe in loop sempre , sia con firefox che con explorer, anche impostanto un altra pagina esistente fuori dal modulo nel metodo submit().

(m: lamefarmer at tiscali.it)

lamefarmer ha detto...

Ho scoperto che
se scrivo
$this->setRedirect(JRoute::_('index.php?option=com_myform', true), ...);
non funziona, esattamente come se aggiunco il path relativo davanti a index, mentre
$this->setRedirect('index.php?option=com_myform', ...);
(cioé senza JRoute) funziona.
Il suggerimento l'ho preso da alcuni moduli Joomla.

Massimo ha detto...

Ho sviluppato il componente sotto Windows, ma con Wamp (quindi webserver Apache) e testato online con Linux+Apache senza riscontrare problemi.

Credo che il tuo problema derivi da IIS e purtroppo con questo non posso aiutarti. Se rimuovendo la chiamata a JRoute hai risolto, segui questa strada.

Ti consiglio però anche una ricerca sul forum ufficiale forum.joomla.org dove ti sarà più facile trovare altri utenti che utilizzano Joomla con IIS.

Penny ha detto...

Volevo chiederti come si fa a salvare in diverse tabelle.
Dato che già non mi è perfettamente chiaro come faccia a salvarlo in una figuriamoci in 2.

Nel senso mi sembra di capire che si crei la tabella tramite variabili virtualmente, le associ al contenuto e poi basta dargli store e fa tutto lui.

Massimo ha detto...

Crei una classe derivata da JTable per ogni tabella come vedi negli esempi.
Poi all' interno di una classe Model puoi scrivere qualcosa del genere:

$tb1 =& $this->getTable('nome_tabella1');
$tb2 =& $this->getTable('nome_tabella2');

E usare i metodi bind, check, store e simili su ognuna delle tabelle.

Oltre a questi articoli dai uno sguardo alla guida dove c'è un esempio un po' più completo.

Marco ha detto...

Complimenti anche per questa seconda versione!
La classe model con varibile interna array strutturata in quel modo è funzionale oltre che per la vista anche per il controller nel passaggio dei dati al modello!
Sono curioso di vedere il seguito!!

Posta un commento

Nota. Solo i membri di questo blog possono postare un commento.