P4A3 framework: porting del codice dell’applicazione per la gestione delle fatture

Logo P4A3Avrei voluto aspettare il rilascio della prima versione definitiva di P4A3,ma non ce l’ho fatta… Era troppa la curiosità! Così ho pensato di provare il porting del codice dell’applicazione invoices manager per cominciare a comprendere le novità del nuovo framework, nonostante la versione di rilascio attuale sia una “preview”. Le differenze sono numerose, anche se l’aspetto esteriore è rimasto sostanzialmente invariato. La prima cosa che salta all’occhio è la differenza nell’utilizzo delle classi. Poiché il nuovo framework si è liberato della compatibilità con il vecchio PHP4, ha guadagnato tutti i benefici della struttura realmente orientata agli oggetti del PHP5. Quindi, per esempio, il costruttore di una classe non prendera più il nome della classe stessa ma sarà definito con: public function __construct(). Con PHP5 inoltre viene finalmente risolto il problema dell’assegnazione nelle variabili oggetto, che avviene sempre per riferimento senza dover anteporre l’operatore &. Infine,
adesso c’è la possibilità di definire proprietà e metodi pubblici, privati e protetti. Sono comunque tantissime le novità e vi rimando al sito di Fabrizio
Balliano
per la lunga lista di cambiamenti!

Tralasciando il codice di index.php che rimane sostanzialmente invariato se non per la riga di codice che richiama la classe base che diventa:
$app = p4a::singleton("Invoices_Manager");
Si nota che non richiede più l’utilizzo di & per l’assegnazione per riferimento all’oggetto. Passiamo a vedere il codice della classe iniziale, quella dove viene definito il menù comune a tutte le maschere:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
class Invoices_Manager extends P4A
{
/**
* @var P4A_Menu
*/
public $menu = null;
/**
* @var P4A_DB_Source
*/
public $clienti = null;
 
public function __construct()
 {
  parent::__construct();
  $this->setTitle("Gestione Fatture");
  // Menu
  $this->build("p4a_menu", "menu");
  $this->menu->addItem("anagrafiche", "Anagrafiche");
  $this->menu->items->anagrafiche->addItem("ditta")
   ->implement("onclick", $this, "menuClick");
  $this->menu->items->anagrafiche->addItem("clienti")
   ->implement("onclick", $this, "menuClick");
  $this->menu->addItem("fatture", "Fatture")
   ->implement("onclick", $this, "menuClick");
  $this->menu->addItem("report", "Report");
  $this->menu->items->report->addItem("lista", "Lista fatture")
   ->implement("onclick", $this, "menuClick");
 
  // Data sources
  $this->build("p4a_db_source", "clienti")
   ->setTable("clienti")
   ->addOrder("Ragione_sociale")
   ->setPageLimit(5)
   ->load();
  // Primary action
  $this->openMask("P4A_Default_Mask");
}
 
 public function menuClick()
 {
  $this->openMask($this->active_object->getName());
 }
}

Si può subito notare la possibilità di utilizzare la sintassi di concatenazione delle istruzioni (chainability, tipo jQuery), la possibilità di utilizzare il metodo implement() senza dover ricorrere prima al metodo addAction() che viene richiamato automaticamente quando l’azione è riconosciuta come evento del browser, infine la possibilità di definire una variabile oggetto pubblica per riutilizzare in più maschere, ad esempio una definizione di sorgente dati.

Per non rendere eccessivamente lungo questo post, mostrerò solo il codice relativo alla maschera che riguarda la fattura, il resto del codice è scaricabile come pacchetto completo in fondo all’articolo.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
class fatture extends P4A_Base_Mask
{
 public $fs_search = null;
 public $txt_search = null;
 public $cmd_search = null;
 public $toolbar = null;
 public $table = null;
 public $fs_details = null;
 
 public function __construct()
 {
  parent::__construct();
  $p4a = P4A::singleton();
  $this->setTitle('Gestione fatture');
  // DB Source
  $this->build("p4a_db_source", "fatture")
    ->setTable("fatture")
    ->addJoin("clienti", "fatture.ID_cliente = clienti.ID",
      array('Ragione_sociale'=>'cliente'))
    ->addOrder("Num_fattura",P4A_ORDER_DESCENDING)
    ->setPageLimit(5)
    ->load();
  $this->setSource($this->fatture);
  $this->firstRow();
  //Create detail source. Detail table should be in a different class
  $dett_prestazioni = $this->build("prestazioni", "dett_prestazioni");
  $dett_prestazioni->source->addFilter("ID_fattura = ?",
  $this->fatture->fields->ID);
  // Customizing fields properties
  $this->setFieldsProperties();
  // Search Fieldset
  $this->build("p4a_field", "txt_search")
   ->setLabel("Numero fattura")
   ->implement("onreturnpress", $this, "search");
  $this->build("p4a_button", "cmd_search")
   ->setLabel("Cerca")
   ->implement("onClick", $this, "search");
  $this->build("p4a_fieldset", "fs_search")
   ->setLabel("Ricerca per numero fattura")
   ->setWidth(800)
   ->anchor($this->txt_search)
   ->anchorLeft($this->cmd_search);
  // Build Tab pane
  $tab_pane = $this->build("p4a_tab_pane","tab_pane");
  // Toolbar
  $this->build("p4a_simple_toolbar", "toolbar")
   ->setMask($this);
  // Hide original print screen button
  $this->toolbar->buttons->print->setInvisible();
  // Add new button to print pdf
  $this->toolbar->addButton('stampafattura','print');
  // Intercept onClick and override with our own method
  $this->intercept($this->toolbar->buttons->stampafattura,
    'onClick','print_doc');
  // Join toolbar with this mask
  // Table
  $this->build("p4a_table", "table")
   ->setWidth(700)
   ->setSource($this->fatture)
   ->setVisibleCols(array("Num_fattura", "Data_fattura", "cliente",
     "Pagamento", "Pagata"))
   ->showNavigationBar();
  $this->table->cols->Num_fattura->setLabel("N. fattura");
  //Fieldset con l'elenco dei campi
  $this->build("p4a_fieldset", "fs_details")
   ->setLabel("Dati fattura")
   ->setWidth(700)
   ->anchor($this->fields->Num_fattura)
   ->anchorLeft($this->fields->Data_fattura)
   ->anchor($this->fields->ID_cliente)
   ->anchorLeft($this->fields->Pagamento)
   ->anchor($this->fields->Pagata);
   $this
    ->setRequiredField("Num_fattura")
    ->setRequiredField("Data_fattura");
   // events onChange
   // Intercept change page on the tab pane
   $tab_pane->implement("afterSetActivePage",$this,"ChangePage");
   // tabpane settings
   $tab_pane->setWidth(750); 
   $tab1 = $tab_pane->pages->build("p4a_frame","tab1"); 
   $tab2 = $tab_pane->pages->build("p4a_frame","tab2"); 
   $tab1->setLabel("Fatture");
   $tab2->setLabel("Dettaglio fattura");
   // anchor tab pane
   $this->frame->anchor($tab_pane);
   // tab1 - Anchor master table
   $tab1
    ->anchor($this->fs_search)
    ->anchor($this->table)
    ->anchor($this->fs_details)
    ->newRow();
   // tab2 - Anchor detail table
   $tab2
    ->anchor($dett_prestazioni->toolbar)
    ->anchor($dett_prestazioni->table)
    ->anchor($dett_prestazioni->fs_details);
   // Display
   $this
    ->display("menu", P4A::singleton()->menu)
    ->display("top", $this->toolbar)
    ->setFocus($this->fields->Num_fattura);
  }
 private function setFieldsProperties()
 {
  $this->fields->ID_cliente
   ->setType("select")
   ->setSource(P4A::singleton()->clienti)
   ->setSourceDescriptionField("Ragione_sociale")
   ->setLabel("Cliente")
   ->setWidth(200);
  }
 public function search()
 {
  $value = $this->txt_search->getSQLNewValue();
  $this->fatture
   ->setWhere(P4A_DB::singleton()->getCaseInsensitiveLikeSQL("Num_fattura",
    "%{$value}%"))
   ->firstRow();
  if (!$this->fatture->getNumRows()) {
   $this->warning("Nessuna fattura trovata");
   $this->fatture->setWhere(null);
   $this->fatture->firstRow();
  }
 }
 public function ChangePage()
 {
  $newpage = $this->tab_pane->getActivePageName();
  switch ($newpage)
   {
    case "tab1":
     // If display master, enable all toolbar buttons
     $this->toolbar->buttons->new->enable();
     $this->toolbar->buttons->save->enable();
     $this->toolbar->buttons->delete->enable();
     $this->toolbar->buttons->print->enable();
     //  force to redesign toolbar
     $this->toolbar->redesign();
     break;
    case "tab2":
     // If display detail, disable some master table buttons
     $this->toolbar->buttons->new->disable();
     $this->toolbar->buttons->save->disable();
     $this->toolbar->buttons->delete->disable();
     $this->toolbar->buttons->print->disable();
     //  force to redesign toolbar
     $this->toolbar->redesign();
     // Go to first row and fill fieldset
     $this->setFocus($this->dett_prestazioni->fields->Descrizione);
     $this->dett_prestazioni->firstRow();
     break;
   }
 }
 public function print_doc()
 // Function to print selected invoice with ezPdf class
 {
  require("class.ezpdf.php");
  $pdf = new Cezpdf("a4","portrait");
  // Select font family
  $pdf->selectFont($_SERVER["DOCUMENT_ROOT"].P4A_APPLICATION_PATH.
   "/libraries/fonts/Helvetica.afm");
  // Define some usefull styling tags (like html)
  $tmp = array(
    "b"=>"Helvetica-Bold.afm"
    ,"i"=>"Helvetica-Oblique.afm"
    ,"bi"=>"Helvetica-BoldOblique.afm"
    ,"ib"=>"Helvetica-BoldOblique.afm"
    ,"bb"=>"Helvetica-Bold.afm");
  $pdf->setFontFamily("Helvetica.afm",$tmp);
  $db = p4a_db::singleton();
  // Get from db header information
  $headerpdf=$db->getAll("SELECT Ragione_sociale, Indirizzo,
                     CONCAT('Tel: ',Telefono),
                     CONCAT('Fax: ',Fax),
                     CONCAT('Email: ',email),
                     CONCAT('C.F.: ',CF),
                     CONCAT('P.IVA: ',PIVA),
                     CONCAT('Banca: ',Dati_bancari)
                     FROM intestazione");
  // Define xy init set up values
  $x=65;
  $y=820;
  $pdf->ezSetY($y);
  // Cycle through fields and print header
  foreach($headerpdf[0] as $key => $value){
   $pdf->ezSetY($y);
   $pdf->ezText($value,10,array("aleft"=>$x,"justification"=>"left"));
   $y-=10;
  }
  // Retrieve actual customer's id
  $actualIdCliente = $this->fatture->fields->ID_cliente->getNewValue();
  // Get customer's data from db
  $invoiceData = $db->getAll("SELECT Ragione_sociale, indirizzo,
                         CONCAT_WS(' - ', CAP, Citta, Provincia),
                         PIVA, CF
                         FROM clienti
                         WHERE ID = '$actualIdCliente'");
  $y-=20;
  $pdf->ezSetY($y);
  $pdf->ezText("Spett.le ",10,array('aleft'=>'300',
                  'justification'=>'left'));
  $x=350;
  // Cycle through fields and print data
  foreach($invoiceData[0] as $key => $value){
   $pdf->ezSetY($y);
   $pdf->ezText($value,10,array('aleft'=>$x,'justification'=>'left'));
   $y-=10;
  }
  // Print invoice number and date (we have it!)
  $numDateInvoice = "Fattura n. <b>".
                     $this-&gt;fields-&gt;Num_fattura-&gt;getValue().
                     "</b> del <b><i>".
                     $this-&gt;fields-&gt;Data_fattura-&gt;getNewValue().
                     "</i></b>";
  $x=65;
  $y-=20;
  $pdf-&gt;ezSetY($y);
  $pdf-&gt;ezText($numDateInvoice,10,array('aleft'=&gt;$x,
                  'justification'=&gt;'left'));
  // Retrieve actual invoice id
  $actualIdFattura = $this-&gt;fatture-&gt;fields-&gt;ID-&gt;getNewValue();
  // Get invoice detail items from db
  $detInvoiceData = $db-&gt;getAll("SELECT Descrizione, Importo,
                            CONCAT(Aliquota_IVA,'%')
                            FROM prestazioni
                            WHERE ID_Fattura = '$actualIdFattura'");
  // Print header fields name
  $y-=30;
  $pdf-&gt;ezSetY($y);
  $pdf-&gt;ezText("Descrizione",12,array('aleft'=&gt;'65',
           'justification'=&gt;'left'));
  $pdf-&gt;ezSetY($y);
  $pdf-&gt;ezText("Importo",12,array('aleft'=&gt;'415', 'aright'=&gt;'485',
           'justification'=&gt;'right'));
  $pdf-&gt;ezSetY($y);
  $pdf-&gt;ezText("IVA",12,array('aleft'=&gt;'515',
           'justification'=&gt;'left'));
  $y-=15;
  $pdf-&gt;line(65,$y,540,$y);
  $x=65;
  $y-=10;
  // Cycle through records and fields and print data
  foreach($detInvoiceData as $row){
   foreach($row as $key =&gt; $value){
    $pdf-&gt;ezSetY($y);
    if($key=="Importo"){
     //format price with 2 decimals
     $formattedValue = sprintf("%01.2f", $value);
     $pdf-&gt;ezText($formattedValue,10,array('aleft'=&gt;$x,
              'aright'=&gt;$x+70,'justification'=&gt;'right'));
    }
    else{
     $pdf-&gt;ezText($value,10,array('aleft'=&gt;$x,
              'justification'=&gt;'left'));
    }
    if($x==65)
     $x+=350;
    else
     $x+=100;
   }
   $x=65;
   $y-=10;
  }
  // Get total amount price and tax with an aggregation query
  $totPriceInvoice = $db-&gt;getAll( "SELECT fatture.ID,
     SUM( prestazioni.importo) AS TotaleImponibile,
     SUM( prestazioni.importo * (prestazioni.Aliquota_IVA /100)) AS TotaleIVA,
     SUM(prestazioni.importo * (1 + (prestazioni.Aliquota_IVA /100))) AS Totale
     FROM fatture
     RIGHT JOIN prestazioni ON fatture.ID = prestazioni.ID_fattura
     GROUP BY fatture.ID HAVING fatture.ID='$actualIdFattura'");
  $y-=25;
  $pdf-&gt;line(65,$y,540,$y);
  // Print a summary with total price without tax, total tax,
  // and total price including tax.
  $y-=50;
  $pdf-&gt;ezSetY($y);
  $pdf-&gt;ezText('<b>Totale imponibile</b>',
                  10,array('aleft'=&gt;'300',
                  'justification'=&gt;'left'));
  $pdf-&gt;ezSetY($y);
  $formattedValue = sprintf("%01.2f",$totPriceInvoice[0]['TotaleImponibile']);
  $pdf-&gt;ezText("<b>".$formattedValue."</b>",10,
           array('aleft'=&gt;'415','aright'=&gt;'485',
           'justification'=&gt;'right'));
  $y-=12;
  $pdf-&gt;ezSetY($y);
  $pdf-&gt;ezText('<b>Totale IVA</b>',10,array('aleft'=&gt;'300',
           'justification'=&gt;'left'));
  $pdf-&gt;ezSetY($y);
  $formattedValue = sprintf("%01.2f",$totPriceInvoice[0]['TotaleIVA']);
  $pdf-&gt;ezText("<b>".$formattedValue."</b>",10,
           array('aleft'=&gt;'415','aright'=&gt;'485',
           'justification'=&gt;'right'));
  $y-=12;
  $pdf-&gt;ezSetY($y);
  $pdf-&gt;ezText('<b>Totale fattura</b>',10,
           array('aleft'=&gt;'300',
           'justification'=&gt;'left'));
  $pdf-&gt;ezSetY($y);
  $formattedValue = sprintf("%01.2f",$totPriceInvoice[0]['Totale']);
  $pdf-&gt;ezText("<b>".$formattedValue."</b>",10,
           array('aleft'=&gt;'415','aright'=&gt;'485',
           'justification'=&gt;'right'));
  // Send pdf to file
  $filename = 'fattura.pdf';
  $pdfcode = $pdf-&gt;output(1); // data stream without header
  P4A_Output_File($pdfcode,$filename);
 }
}

Come è possibile notare ho potuto lasciare gran parte del codice invariato, però sono riuscito a diminuire le istruzioni. Ho eliminato gran parte della formattazione degli output in tabella e nei fieldset grazie ad un più facile riconoscimento dei tipi del db source. Per questo motivo ho anche cambiato, nell’archivio MySQL, il tipo della data da timestamp a date . Altra facilitazione è data dalla costruzione automatica del frame principale. Infine, è interessante la possibilità di utilizzare la funzione P4A_Output_File() che permette di generare un file temporaneo e reinderizzarlo al browser in un porcesso separato, davvero comodo per generare i report PDF!

Note:

L’utilizzo della funzione P4A_Output_File() generava un piccolo problema sui server Windows, a causa di una incorretta traduzione degli slash in controslash. Il problema è stato già risolto, ma non ancora rilasciato nell’attuale versione 2.99.8. Per ottenere il corretto funzionamento in Windows, quindi è necessario prelevare i seguenti sorgenti dal SVN: p4a.php versione 1745, functions.php versione 1746 e constants.php versione 1746 oppure aspettare il nuovo imminente rilascio.

Un ultimo piccolo particolare che non sono riuscito per ora a risolvere: il file PDF temporaneo non viene cancellato alla fine della sessione, probabilmente ho sbagliato qualcosa io… cercherò di risolverlo presto!

Download

Il pacchetto completo delle librerie per la generazione del PDF, i file .sql per il database e tutti i sorgenti e l’albero delle directory è scaricabile qui.

Conclusioni:

Lungi da me pensare di aver imparato a lavorare con il nuovo framework, il primo approccio è stato molto più semplice del previsto! Bisogna comunque studiarlo ancora bene, e generare tanta documentazione. Credo che sia uno sforzo al quale tutti gli utilizzatori del lavoro di Balliano e Giardina debbano fare. Ho approcciato la vecchia versione quando ormai c’era già parecchia documentazione sul forum e ho imparato in fretta parecchie cose. Adesso però, è necessario ricominciare, non da capo… diciamo da tre (citando l’indimenticabile Troisi!) e produrre codice e documentazione, tutti insieme per la crescita del framework.

Riferimenti ed approfondimenti:

P4A framework: utilizzare il widget P4A Google Maps per implementare uno store locator

 

Questo post ha più di 1 anno. Il contenuto potrebbe essere obsoleto, non più completamente accessibile o mancante di alcune informazioni.

P4A esempio di Store LocatorEcco un esempio di utilizzazione del widget P4A Google Maps per creare una mappa di riferimenti (negozi, magazzini, ecc) che siano compresi in un certo raggio di distanza in Km, a partire da un indirizzo prestabilito.
Ho cercato di utilizzare il widget di Alberto Galanti senza effettuare nessuna modifica al codice originale. Ho scelto, quindi di implementare un metodo che fornisce la georeferenza della località che funge da centro, per poi effettuare, attraverso una query, la selezione degli indirizzi compresi nel raggio di distanze prestabilito.
L’esempio utilizzato, per comodità, è lo stesso indicato dalla guida di Google Maps in questo articolo.

I passi principali prima di procedere:

  • Scaricare il widget P4A Google Maps qui
  • Creare un archivio di riferimenti che abbiano un indirizzo nel formato accettato da Google Maps (Es.: “Piazza Venezia, Roma, Italia”)
  • Georeferenziare tutti gli indirizzi (trovare latitudine e longitudine)

Utilizzando MySQL, la struttura dell’archivio suggerita da Google è questa:

1
2
3
4
5
6
7
8
CREATE TABLE `markers` (
  `id` INT(11) UNSIGNED NOT NULL AUTO_INCREMENT,
  `name` VARCHAR(60) NOT NULL,
  `address` VARCHAR(80) NOT NULL,
  `lat` FLOAT(10,6) NOT NULL,
  `lng` FLOAT(10,6) NOT NULL,
  PRIMARY KEY  (`id`)
) ENGINE=MyISAM  DEFAULT CHARSET=latin1;

Per comodità potete scaricare l’sql completo dei dati di esempio di Google qui

Nel caso non si conoscano già le coordinate dei riferimenti, è possibile utilizzare il metodo suggerito in questo articolo sempre da Google. Questo sistema permette di aggiornare direttamente i dati di un database MySQL con le coordinate fornite da Google Maps per tutti gli indirizzi memorizzati.

La selezione dei record che sono compresi in un reggio di distanza in Km prefissato, avviene tramite una query che utilizza la formula della triangolazione sferica:
cos p = cos a cos b + sen a sen b cos φ,
per maggiori dettagli potete leggere questo post.

Ecco la query in pratica:

1
2
3
4
5
6
7
8
SELECT address, name, lat, lng, 
( 6371 * acos( cos( radians('Center_latitude')) * cos( radians( lat )) 
* cos( radians( lng ) - radians('Center_longitude')) 
+ sin( radians('Center_latitude'))
* sin( radians( lat )))) AS distance
FROM markers
HAVING distance &lt; 'Radius'
ORDER BY distance

dove 'Center_latitude' è la latitudine del centro, 'Center_longitude' è la longitudine del centro e 'Radius' è il raggio in km dal centro.

Ed ecco dunque, il codice della maschera P4A:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
class GoogleMap extends P4A_Mask
{
  function GoogleMap()
  {
    parent::P4A_Mask();
    $this-&gt;setTitle("Google Map Store Locator Test");
    // Frame
    $this-&gt;build("p4a_frame", "frame");
    $this-&gt;frame-&gt;setWidth(800);
    //la chiave per localhost è disponibile nel file p4a_map.php
    $coordArray = $this-&gt;getLocationCoordinates("Mountain View, CA", 
                  "Inserire_chiave_Google"); 
    $radius = 12;
    // DB Source
    $this-&gt;build("p4a_db_source", "source");
    $query = "SELECT address, name, lat, lng, 
     CONCAT_WS('
', name, address) AS info,
     (6371 * acos(cos(radians('$coordArray[2]')) * cos(radians(lat))
     * cos( radians(lng) - radians('$coordArray[3]')) 
     + sin( radians('$coordArray[2]'))
     * sin( radians(lat)))) AS distance
     FROM markers
     HAVING distance &lt; '$radius' ORDER BY distance"; $this-&gt;source-&gt;setQuery($query);
    $this-&gt;source-&gt;setPk("id");
    $this-&gt;source-&gt;load();
    // map
    $map =&amp; $this-&gt;build("p4a_map","mappa");
    $map-&gt;setCenter("Mountain View, CA");
    $map-&gt;setZoom(10);
    $map-&gt;setWidth(750);
    $map-&gt;setHeight(500);
    // markers from datasource
    $map-&gt;setSourceAddressField("address");
    //$map-&gt;setSourceDescriptionField("distance");
    $map-&gt;setSourceDescriptionField("info");
    $map-&gt;setSource($this-&gt;source);
    // Layout
    $this-&gt;frame-&gt;anchorCenter($map);
    // Display
    $this-&gt;display("main", $this-&gt;frame);
   }
 /*
  * Return the coordinates of the location.
  * @param $location		valid Google Maps address of a location.
  * @param $googleMapKey		key for accessing Google Maps.	 
  * @return array  element[0] = status, element[2] = lat, element[3] = long
  */
 function getLocationCoordinates($location, $googleMapKey)
 {
  $base_url = "http://maps.google.com/maps/geo?output=csv&amp;key=".$googleMapKey;
  $request_url = $base_url."&amp;q=".urlencode($location);
  $csv = file_get_contents($request_url);
  if ($csv) {
    $csvSplit = split(",", $csv);
  }
  else{
    $csvSplit = array();
  }
    return $csvSplit;
  }
}

Note:

La query crea la sorgente dati per i markers, che vengono evidenziati automaticamente dalla classe p4a_map.
Al posto delle info, potete visualizzare la distanza di ogni marker dal centro (nell’esempio: “Mountain View, CA”) utilizzando l’istruzione: $map->setSourceDescriptionField("distance");.
Il metodo getLocationCoordinates() recupera le coordinate del centro per poter poi comporre la query.
La chiave per l’accesso a Google Map è quella valida per un server tipo “localhost” e la trovate all’interno del file: p4a_map.php che si trova nella directory libraries. Per avere quella per un dominio pubblico è necessario farla generare a Google Maps qui.
Non ho fatto controlli di errore, ad esempio per quanto riguarda la risposta del metodo getLocationCoordinates() sarebbe opportuno controllare che l’array non sia vuoto e anche lo status della risposta di Google Maps, e si dovrebbe anche limitare il risultato della query al numero di markers massimo gestiti dal widget (17 se non ricordo male…).

Download

Il widget con la maschera sopra descritta è scaricabile qui

Conclusioni:

Il risultato è già soddisfacente, anche se un po’ grezzo, ma credo ci siano molti margini di miglioramento per questo tipo di utilizzo di Google Maps in P4A. Spero di avere tempo per affinare ulteriormente sia questo programma che il widget, per renderlo utilizzabile anche per idee diverse! Suggerimenti e consigli sono molto ben accetti!!

Riferimenti ed approfondimenti:

P4A framework: gestire maschere master-detail multiple con tab e div a scomparsa

schema databaseVorrei proporre un esempio un po’ particolare, ma non raro, di gestione di maschere nel caso la classica situazione master-detail sia leggemente più complessa.
Immaginiamo di avere la tabella master degli ordini e la tabella detail del dettaglio dell’ordine. Supponiamo però che il nostro utente abbia necessità di impostare nell’ordine un certo modello (si può pensare ad un kit) di prodotto al quale legare un certo numero di componenti e di accessori.
Per riassumere in un database sufficientemente normalizzato, questo tipo di informazione, si può ricorrere allo schema illustrato nella figura a fianco. In questo modo la chiave primaria della tabella dettaglio_ordine, verrà associata ad ogni singolo modello dell’ordine, e per ogni modello sarà possibile associare un certo numero di componenti ed accessori attraverso le rispettive tabelle di relazione.

Come rendere adesso ergonomica la visualizzazione delle maschere? La maschera dettaglio_ordine, infatti dovrebbe contenere anche dettaglio_componenti e dettaglio_accessori.
applicazione di provaLa soluzione che ho scelto, è di sistemare la master ordini in un tab, e tutte le detail in un altro tab dove, attraverso un combo box, sia possibile visualizzare a scelta la maschera dettaglio_componenti oppure la dettaglio_accessori o tutte e due, lasciando solo la gestione dei modelli sempre in evidenza.
Per realizzare questa soluzione, bisogna ancorare tutti i componenti (tabella, fieldset e toolbar) delle maschere per gestire componenti e accessori in due appositi frame e impostare il metodo setVisible() a FALSE o a TRUE rispettivamente per nascondere o mostrare la maschera. Per prima cosa, nel costruttore della classe principale, definiamo le source per le maschere secondarie (per brevità inserisco qui solo la parte di codice relativa ai div a scomparsa, ma tutto il codice è comunque scaricabile in fondo a questo articolo)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
//.......
//Build main detail source
$dett_ordine= &amp; $this-&gt;build("dettaglio_ordine", "dett_ordine");
$dett_ordine-&gt;dettaglio_ordine-&gt;addFilter("ordini_idordini = ?", 
                                      $this-&gt;ordini-&gt;fields-&gt;idordini); 
$this-&gt;dett_ordine-&gt;dettaglio_ordine-&gt;firstRow();
 
//Build component detail source
$cmpntidtglo= &amp; $this-&gt;build("componenti_dettaglio", "cmpntidtglo");    
$cmpntidtglo-&gt;componenti_dettaglio-&gt;addFilter("dettaglio_ordine_iddettaglio_ordine = ?", 
                      $this-&gt;dett_ordine-&gt;dettaglio_ordine-&gt;fields-&gt;iddettaglio_ordine); 
$cmpntidtglo-&gt;componenti_dettaglio-&gt;firstRow();
 
//Build accessories detail source
$accesrdtglo= &amp; $this-&gt;build("accessori_dettaglio", "accesrdtglo");
$accesrdtglo-&gt;accessori_dettaglio-&gt;addFilter("dettaglio_ordine_iddettaglio_ordine = ?", 
                      $this-&gt;dett_ordine-&gt;dettaglio_ordine-&gt;fields-&gt;iddettaglio_ordine); 
$accesrdtglo-&gt;accessori_dettaglio-&gt;firstRow();
//.......

Poi costruiamo i due frame per ancorarci le maschere componenti e accessori:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//.......
// Frame order detail (main)
$frm=&amp; $this-&gt;build("p4a_frame", "frm");
$frm-&gt;setWidth(800);
// Frame component
$frmcmpntidtglo=&amp; $this-&gt;build("p4a_frame", "frmcmpntidtglo");
$frmcmpntidtglo-&gt;setWidth(700);
$frmcmpntidtglo-&gt;anchor($cmpntidtglo-&gt;table_componenti_dettaglio); 
$frmcmpntidtglo-&gt;anchor($cmpntidtglo-&gt;tool);
$frmcmpntidtglo-&gt;anchor($cmpntidtglo-&gt;frm);  
// Frame accessories
$frmaccesrdtglo=&amp; $this-&gt;build("p4a_frame", "frmaccesrdtglo");
$frmaccesrdtglo-&gt;setWidth(700);
$frmaccesrdtglo-&gt;anchor($accesrdtglo-&gt;table_accessori_dettaglio); 
$frmaccesrdtglo-&gt;anchor($accesrdtglo-&gt;tool);
$frmaccesrdtglo-&gt;anchor($accesrdtglo-&gt;frm);
//.......

Costruiamo il combo box per la scelta di visualizzazione

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
//.......
//Fieldset + combo to manage views
$fsetShowHide=&amp; $this-&gt;build("p4a_fieldset", "fsetShowHide");
$fsetShowHide-&gt;setWidth(700);
$fsetShowHide-&gt;setTitle("Visualizzazione");
$array_source=&amp; $this-&gt;build("P4A_Array_Source", "array_source");
$a = array(); 
$a[] = array('id'=&gt;1, 'value'=&gt;'nascondi tutto'); 
$a[] = array('id'=&gt;2, 'value'=&gt;'dettaglio componenti');
$a[] = array('id'=&gt;3, 'value'=&gt;'dettaglio accessori');
$a[] = array('id'=&gt;4, 'value'=&gt;'mostra tutto');
$array_source-&gt;setPk('id'); 
$array_source-&gt;load($a); 		
$combo_showhide =&amp; $this-&gt;build("p4a_field","combo_showhide");
$combo_showhide-&gt;setType("select");
$combo_showhide-&gt;setSource($array_source);
$combo_showhide-&gt;setLabel("Mostra");
$fsetShowHide-&gt;anchor($combo_showhide);
//.......

Intercettiamo l’evento change del combo box

1
2
3
4
5
6
//.......
// events onChange 
$combo_showhide-&gt;addAction("onChange");
//Intercept showhide combo action
$this-&gt;intercept($combo_showhide,"onChange","ShowHide");
//.......

Ancoriamo tutto nel secondo tab:

1
2
3
4
5
6
7
8
//.......
$tab2-&gt;anchor($fsetShowHide);
$tab2-&gt;anchor($dett_ordine-&gt;frm);  
$tab2-&gt;anchor($frmcmpntidtglo);
$tab2-&gt;anchor($frmaccesrdtglo);
$frmcmpntidtglo-&gt;setVisible(FALSE);    
$frmaccesrdtglo-&gt;setVisible(FALSE);
//.......

Infine definiamo il metodo per la visualizzazione dei frame

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
//.......
  function showHide()
  {
    $idDettOrd=$this-&gt;dett_ordine-&gt;dettaglio_ordine-&gt;fields-&gt;
                                iddettaglio_ordine-&gt;getValue();
    // Cannot show accessories or components tables if thera are no records in
    // models table
    if (empty($idDettOrd))
        {
          $res=1; // Hide all
          $comboValue=$this-&gt;combo_showhide-&gt;getNewValue();
          if($comboValue&gt;1)
          {
          $this-&gt;dett_ordine-&gt;message-&gt;setValue("Impossibile visualizzare 
                                      dettagli: nessun modello presente!");
          $this-&gt;combo_showhide-&gt;setValue(1); // Hide all                                           
          }                                           
        }
     // If there are records in models table
        else
        {
          $res=$this-&gt;combo_showhide-&gt;getNewValue();
        }
    switch ($res)
    {
      case 1: // Hide all
          $this-&gt;frmcmpntidtglo-&gt;setVisible(FALSE);
          $this-&gt;frmaccesrdtglo-&gt;setVisible(FALSE);      
      break;
      case 2: // Show components
          $this-&gt;frmcmpntidtglo-&gt;setVisible(TRUE);
          $this-&gt;frmaccesrdtglo-&gt;setVisible(FALSE);
      break;
      case 3: // Show accessories
          $this-&gt;frmcmpntidtglo-&gt;setVisible(FALSE);
          $this-&gt;frmaccesrdtglo-&gt;setVisible(TRUE);
      break;
      case 4: // Show all
          $this-&gt;frmcmpntidtglo-&gt;setVisible(TRUE);
          $this-&gt;frmaccesrdtglo-&gt;setVisible(TRUE);                
      break;
    }
  }
//.......

C’è da notare che questo sistema risulta utile anche perché il metodo addFilter(), nel caso il parametro passato sia NULL, restituisce un insieme di record non filtrati. Nel nostro caso, quando viene registrato un nuovo ordine, ma non è stato ancora inserito nessun modello, l’identificativo (iddettaglio_ordine) nella tabella dettaglio_ordine non esiste ancora, e quindi le tabelle non filtrate mostrerebbero tutti i record, cosa sgradevole e soprattutto pericolosa.

Raccomandazioni:

Questa applicazione è solo un esercizio, un test per provare alcune soluzioni di visualizzazione delle maschere, non è quindi dotata di particolari controlli indispensabili in produzione. Non mi assumo nessuna responsabilità su un suo possibile cattivo funzionamento. Accetto, invece, volentieri eventuali feedback o proposte alternative!

Download:

pacchetto

Conclusioni:

Proprio oggi Fabrizio Balliano ha annunciato il rilascio della prima pre-versione di P4A3.
Questo mi ha indotto a non approfondire eccessivamente le soluzioni prospettate in questo articolo, in quanto la nuova versione è significativamente differente dalla vecchia. Probabilmente con la nuova versione molte soluzioni avranno una via diversa e, da quanto ho capito finora, ancora più semplice.
Infine, si annuncia una cosa molto gradita: una maggiore velocità di tutte le applicazioni sotto il profilo delle prestazioni!

Riferimenti ed approfondimenti:

P4A framework: utilizzo di multicheckbox e multiselect con tabelle di relazione N:N

screenshot della multicheckboxUn tipico esempio di utilizzo di un multicheckbox o di una multiselect si ha quando è necessario creare una maschera per una tabella di relazione N:N. Nel framework P4A è possibile implementare entrambi i tipi di oggetto.

Il problema è che, come detto in precedenti post, la documentazione non è completa, si rende necessario quindi fare una una ricerca nel forum. In questo thread e in quest’altro, viene spiegato il funzionamento. Sperando di fare cosa gradita, ho cercato di riassumere la procedura in questo esempio pratico.

 

schema del database Dunque, ammettiamo di avere tre tabelle relazionate come nella figura a fianco. L’obiettivo è di poter associare ad ogni fascia uno o più colori. Nella tabella di relazione possono essere presenti N fasce e per ogni fascia N colori. Per prima cosa creaiamo le tre tabelle:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
CREATE TABLE `colori` (
  `idcolori` BIGINT(20) NOT NULL AUTO_INCREMENT,
  `nome` VARCHAR(255) NOT NULL,
  `note` VARCHAR(255) DEFAULT NULL,
  PRIMARY KEY  (`idcolori`),
  UNIQUE KEY `colori_index_nome` (`nome`)
) ENGINE=InnoDB  DEFAULT CHARSET=latin1;
-- -----------------------------------------------
CREATE TABLE `fasce` (
  `idfasce` BIGINT(20) NOT NULL AUTO_INCREMENT,
  `descrizione` VARCHAR(255) DEFAULT NULL,
  PRIMARY KEY  (`idfasce`)
) ENGINE=InnoDB  DEFAULT CHARSET=latin1;
-- -----------------------------------------------
CREATE TABLE `fasce_colori` (
  `colori_idcolori` BIGINT(20) NOT NULL,
  `fasce_idfasce` BIGINT(20) NOT NULL,
  PRIMARY KEY  (`colori_idcolori`,`fasce_idfasce`),
  KEY `fasce_colori_FKIndex1` (`colori_idcolori`),
  KEY `fasce_colori_FKIndex2` (`fasce_idfasce`)
) ENGINE=InnoDB DEFAULT CHARSET=latin1;

A questo punto è necessario creare le due classi e le maschere per la gestione delle tabelle fasce e colori nel modo standard, che non illustro ma sono presenti nel pacchetto scaricabile.
Possiamo finalmente creare la maschera per la tabella di relazione: fasce_colori nel seguente modo:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
class P4A_Fasce_colori extends P4A_Mask
{
 function P4A_Fasce_colori()
 {
 $this-&gt;P4A_Mask();
 $p4a = &amp; P4A::singleton();
 // DB Source
 $this-&gt;build("p4a_db_source","fasce");
 $this-&gt;fasce-&gt;setTable("fasce");
 $this-&gt;fasce-&gt;setPk("idfasce");
 //Filtro il record con idfasce=0 che indica
 //tutti i colori e non deve essere modificato
 $this-&gt;fasce-&gt;setWhere("idfasce &gt; 0");
 $this-&gt;fasce-&gt;setPageLimit(15);
 $this-&gt;fasce-&gt;addMultivalueField("fld_fasce_colori",
  "fasce_colori","fasce_idfasce", "colori_idcolori");
 $this-&gt;fasce-&gt;load();
 $this-&gt;setSource($this-&gt;fasce);
 $this-&gt;setTitle('Gestione fasce colore');
 $this-&gt;fasce-&gt;firstRow();
 $this-&gt;build("p4a_db_source","colori");
 $this-&gt;colori-&gt;setTable("colori");
 $this-&gt;colori-&gt;setPk("idcolori");
 $this-&gt;colori-&gt;load();
 $this-&gt;fields-&gt;fld_fasce_colori-&gt;setType("multicheckbox");
 // Oppure se si preferisce la multiselect
 //$this-&gt;fields-&gt;fld_fasce_colori-&gt;setType("multiselect");
 $this-&gt;fields-&gt;fld_fasce_colori-&gt;setSource($this-&gt;colori);
 $this-&gt;fields-&gt;fld_fasce_colori-&gt;label-&gt;setVisible(false);
 //Fieldset con l'elenco dei campi
 $this-&gt;build("p4a_fieldset", "fset");
 $this-&gt;fset-&gt;setWidth(590);
 $this-&gt;fset-&gt;setTitle("Selezione colori");
 $this-&gt;fset-&gt;anchor($this-&gt;fields-&gt;fld_fasce_colori);
 // Toolbar
 $this-&gt;build("p4a_simple_toolbar", "toolbar");
 $this-&gt;toolbar-&gt;setMask($this);
 $this-&gt;toolbar-&gt;buttons-&gt;new-&gt;disable();
 // Table
 $table = &amp; $this-&gt;build("p4a_table", "table");
 $table-&gt;setWidth(600);
 $table-&gt;setSource($this-&gt;fasce);
 $table-&gt;setVisibleCols(array("descrizione"));
 $table-&gt;showNavigationBar();
 // Message
 $message = &amp; $this-&gt;build("p4a_message", "message");
 $message-&gt;setWidth("300");
 // Frame
 $frm= &amp; $this-&gt;build("p4a_frame", "frm");
 $frm-&gt;setWidth(600);
 $frm-&gt;anchorCenter($message);
 $frm-&gt;anchorCenter($table);
 $frm-&gt;anchorCenter($this-&gt;fset);
 // Display
 $this-&gt;display("main", $frm);
 $this-&gt;display("menu", $p4a-&gt;menu);
 $this-&gt;display("top", $this-&gt;toolbar);
 }
}

Notate che la sorgente dati principale è quella della tabella dove agisce il selettore dei record,
nel nostro caso: fasce. L’azione chiave di tutta la procedura è la costruzione
di un campo attraverso il metodo addMultivalueField(),
i quattro parametri passati nell’ordine a questo metodo sono:

  • Il nome del nuovo campo
  • Il nome della tabella di relazione
  • Il nome della chiave esterna che lega la tabella dove agisce il selettore record
  • Il nome della chiave esterna che lega la tabella che funge da sorgente dati per il nuovo campo

Richiamando il metodo saveRow() le query di aggiornamento e inserimento verranno automaticamente eseguite dal P4A framework engine senza dover scrivere ulteriori righe di codice!

Download:

Il pacchetto completo è scaricabile qui

Conclusioni:

Il setup del codice necessario all’utilizzo di multicheckbox e multiselect è semplice ma poco documentato, il suo utilizzo è potente e fa risparmiare parecchio tempo.

Riferimenti ed approfondimenti:

P4A framework: modificare il comportamento del bottone print nella simple toolbar

screenshot della simple toolbarIl widget simple toolbar del framework P4A include un bottone predefinito print per la funzione di stampa. Questa funzione, per default, esegue in javascript una semplice window.print() della maschera, ma in molti casi è necessario personalizzare la stampa.
Qui nasce un piccolo problema: mentre per gli altri bottoni, è possibile effettuare semplicemente l’override del metodo, con il print è necessario agire diversamente.

Il motivo risulta chiaro osservando il codice della classe base P4A_Simple_Toolbar in simple.php:

1
2
3
4
5
6
// ...
$print = & $this->addButton('print', 'print');
$print->dropAction('onclick');
$print->setProperty('onclick', 'window.print(); return false;');
$print->setAccessKey("P");
// ...

La chiamata al metodo dropAction() resetta qualsiasi altra azione, ecco il codice del metodo, tratto dalla classe base P4A_Widget in widget.php

1
2
3
4
5
6
7
// ...
function dropAction($action)
{
 $action = strtolower($action);
 unset($this->actions[$action]);
}
// ...

Successivamente il metodo setProperty() assegna la funzione javascript window.print() all’evento onClick() del bottone print.
Come aggirare l’ostacolo? Il sistema più semplice è quello di nascondere il bottone print di default, e crearne uno nuovo, avendo cura di passare come secondo parametro, la stringa “print” al metodo addButton() che identifica il nome dell’icona predefinita (print.png)

1
2
3
4
5
6
7
8
9
10
11
// Toolbar
$tool = & $this->build("p4a_simple_toolbar", "tool");
// Hide original print screen button
$tool->buttons->print->setInvisible();
// Add new button to print pdf
$tool->addButton('stampafattura','print');
// Intercept onClick and override with our own method
$this->intercept($tool->buttons->stampafattura,
                 'onClick','print_doc');
// Join toolbar with this mask
$tool->setMask($this);

Il primo parametro passato ad addButton() assegna il nome al nuovo bottone, quindi possiamo intercettare l’evento associato onClick() abbinadogli un nostro metodo (nell’esempio: print_doc() )

1
2
3
4
5
6
function print_doc()
{
 //...
 // Code to customize print
 //...
}

Conclusioni:

Gli americani direbbero: “a piece of cake” per ottenere la personalizzazione del bottone print della simple toolbar senza mettere mano al codice delle classi base del framework!

Riferimenti ed approfondimenti:

P4A framework: esempio di applicazione completa per la gestione delle fatture

Screenshot dell'applicazioneL’idea di realizzare questo esempio di applicazione in P4A è nata per comparare i tempi di sviluppo in un ambiente RAD di tipo WYSIWYG (MS Visual Basic) che genera desktop applications e un framework PHP che genera web applications. Dei vantaggi delle applicazioni web ho già scritto in un precedente articolo.
Il progetto è molto semplice: un programma che gestisce la fatturazione di una ditta che fa servizi, quindi senza magazzino.
Ho scelto questo progetto perché la realizzazione non presenta grosse difficoltà e può essere utile a qualcuno, ed anche perché ci sono dentro diversi aspetti che ricorrono anche in applicazioni più grandi, come ad esempio la gestione di tabelle master-detail e lo sviluppo di un report personalizzato in pdf.
Inoltre lo schema del database è pressoché identico a quello di un tutorial per l’approccio ai dati con Visual Basic .Net 2005 (Visual Basic 2005 passo-passo: Accesso ai dati),
in questo modo sarà più facile effettuare il confronto.
Va detto che non ho ancora realizzato l’equivalente in VB.Net, ma avendo una certa esperienza di VB6 posso dire quanto tempo avrei impiegato a realizzarlo: diciamo 2 giorni al massimo. Ebbene, considerando che la documentazione di P4A è piuttosto scarsa, che il framework non ha un’interfaccia grafica WYSIWYG con strumenti drag’n’drop e wizard per la creazione dei report, e infine, che non ho una conoscenza dello stesso paragonabile all’esperienza che ho con Visual Basic, mi sarei aspettato di impiegare come minimo il doppio del tempo, invece l’applicazione completa è stata realizzata in soli 2 giorni di lavoro nemmeno troppo assiduo (non c’erano clienti col fiato sul collo!).
Avessi voluto usare solo PHP e javascript, ci avrei messo davvero molto di più…!
Un buon risultato, visto che sappiamo quanto sia importante produrre applicazioni in breve tempo per tenere bassi i costi ed accontentare clienti sempre meno disposti ad investire grosse cifre sul software.

Quali sono le caratteristiche dell’esempio?

  • Una maschera per la gestione dell’intestazione della ditta
  • Una maschera per la gestione dell’anagrafica clienti (con ricerca per nome)
  • Una maschera per la gestione delle fatture divisa in dati fattura e dettaglio prestazioni
  • Una procedura di stampa della fattura in formato pdf
  • Una maschera per il riepilogo delle fatture emesse e la possibilità di filtraggio per periodo

Qualche screenshot…

maschera iniziale
maschera iniziale
intestazione della ditta
intestazione della ditta
anagrafica clienti
anagrafica clienti
gestione delle fatture: master
gestione delle fatture: master
gestione delle fatture: detail
gestione delle fatture: detail
stampa pdf
stampa pdf
riepilogo delle fatture
riepilogo delle fatture

 

Note tecniche:
Ho provato l’applicazione sia con P4A 2.1.7 che con l’ultima release 2.2.2.
Il funzionamento è corretto sia con PHP4.x e MySQL4.x che con PHP5.2.4 e MySQL5.0.x, in quest’ultimo caso però è necessario impostare la direttiva del php.ini: memory_limit = 64M, il default è 8M.
All’interno del pacchetto, nella directory ‘_private’ ci sono i dump .sql del database MySQL effettuati con phpMyAdmin e con Heidi, il nome del database deve essere: ‘base_app_invoices’.
Infine per la generazione del report in pdf, ho utilizzato la classe R&OS (pdf-php su Sourceforge), conosciuta anche come ezPdf e licenza di tipo Public Domain

Download:

Pacchetto zip completo di librerie R&OS

Conclusioni:

Se il rapporto 1:1 dei tempi di sviluppo con queste differenti tecnologie rimane lo stesso anche per applicazioni complesse, e constatato il vantaggio delle applicazioni web, è da considerare seriamente l’adozione di questo framework PHP fra i propri strumenti di lavoro.

Riferimenti ed approfondimenti: