Yii

Page last edited 1 221 giorni 12 ore fa
Da wiki.betaingegneria.it.

<a name="top"></a>

Yes, It Is![modifica]

Yii framework, cantiere, armamentario, per la creazione di applicazioni Web 2.0.

Nuova applicazione[modifica]

 marcob@jsbach:~$ cd www/yii/framework
 marcob@jsbach:~/www/yii/framework$ ./yiic webapp ../../jbook
 PHP Warning:  Module 'oci8' already loaded in Unknown on line 0
 Create a Web application under '/var/www/jbook'? [Yes|No] Yes

Questo crea tutto il sistema di cartelle nella directory /var/www/jbook/ Per la creazione del pattern MVC, occorre collegarsi al database: Editare

 /var/www/jbook/protected/config/main.php

alla riga corripsondente 'db'=>array( personalizzare la stringa di connessione e commentare sqlite.

Modificare anche adminEmail

 'adminEmail'=>'marco at betaingegneria dot it'

Siccome si deve usare gii da interfaccia web per fare la costruzione del modello, abilitare, sempre nello stesso file, il tool gii togliendo il commento alla linea 22

  'modules'=>array(
     // uncomment the following to enable the Gii tool
     'gii'=>array(
        'class'=>'system.gii.GiiModule',
        'password'=>'sec4et',
         // If removed, Gii defaults to localhost only. Edit carefully to taste.
        'ipFilters'=>array('127.0.0.1','::1'),
     ),
  ),


 http://jsbach/jbook/index.php?r=gii

Poiché Apache deve scrivere su alcune di queste cartelle, ne modifichiamo il proprietario, con il comando

 marcob@jsbach:~/www/jbook$ sudo chown -R www-data protected/

da

 marcob@jsbach:~/www/jbook$ ll
 totale 44
 drwxr-xr-x   7 marcob marcob  4096 2011-10-26 16:22 ./
 drwxr-xr-x 122 marcob root   12288 2011-10-26 16:22 ../
 drwxrwxrwx   4 marcob marcob  4096 2011-10-26 16:47 assets/
 drwxr-xr-x   2 marcob marcob  4096 2011-10-26 16:22 css/
 drwxr-xr-x   2 marcob marcob  4096 2011-10-26 16:22 images/
 -rw-r--r--   1 marcob marcob   463 2011-10-26 16:22 index.php
 -rw-r--r--   1 marcob marcob   466 2011-10-26 16:22 index-test.php
 drwxr-xr-x  14 marcob marcob  4096 2011-10-26 16:22 protected/
 drwxr-xr-x   3 marcob marcob  4096 2011-10-26 16:22 themes/

a

 marcob@jsbach:~/www/jbook$ ll
 totale 44
 drwxr-xr-x   7 marcob   marcob  4096 2011-10-26 16:22 ./
 drwxr-xr-x 122 marcob   root   12288 2011-10-26 16:22 ../
 drwxrwxrwx   4 marcob   marcob  4096 2011-10-26 16:47 assets/
 drwxr-xr-x   2 marcob   marcob  4096 2011-10-26 16:22 css/
 drwxr-xr-x   2 marcob   marcob  4096 2011-10-26 16:22 images/
 -rw-r--r--   1 marcob   marcob   463 2011-10-26 16:22 index.php
 -rw-r--r--   1 marcob   marcob   466 2011-10-26 16:22 index-test.php
 drwxr-xr-x  14 www-data marcob  4096 2011-10-26 16:22 protected/
 drwxr-xr-x   3 marcob   marcob  4096 2011-10-26 16:22 themes/

Nota bene[modifica]

Così facendo mi sono chiuso fuori, non riesco più a salvare:

 marcob@jsbach:/var/www/factotum$ sudo chmod -R g+w protected/
 marcob@jsbach:/var/www/factotum$ ll
 totale 44
 drwxr-xr-x   7 marcob   marcob  4096 2012-01-28 18:08 ./
 drwxr-xr-x 126 marcob   root   12288 2012-01-28 18:08 ../
 drwxrwxrwx   2 marcob   marcob  4096 2012-01-28 18:08 assets/
 drwxr-xr-x   2 marcob   marcob  4096 2012-01-28 18:08 css/
 drwxr-xr-x   2 marcob   marcob  4096 2012-01-28 18:08 images/
 -rw-r--r--   1 marcob   marcob   463 2012-01-28 18:08 index.php
 -rw-r--r--   1 marcob   marcob   466 2012-01-28 18:08 index-test.php
 drwxrwxr-x  14 www-data marcob  4096 2012-01-28 18:08 protected/ <--- faccio così
 drwxr-xr-x   3 marcob   marcob  4096 2012-01-28 18:08 themes/

gii[modifica]

Gii è un tool per la cpostruzione dello Scaffold.

Prima di lanciarlo occorre compilare il file di configurazione dell'applicazione, in particolare

   'db'=>array(
   	'connectionString' => 'mysql:host=localhost;dbname=cst;',
   	'emulatePrepare' => true,
   	'username' => 'root',
   	'password' => 'root01',
   	'charset' => 'utf8',
   ),

Fare girare Gii, si avrà un menu con gli oggetti da creare; si parte con Model generator. Si fa Preview e poi Generate, a meno di errori sui permessi la cosa va a buon fine. Il modello è così creato.

Uso dei prefissi nella tabelle[modifica]

Si possono usare prefissi alle tabelle per far convivere più sottoschemi all'interno di un solo schema. Buono per ambienti promiscui.

Editare il file protected/config/main.php e aggiungere la riga

 tablePrefix' => 'jb_',

qui

 db'=>array(
       'class'=>'CDbConnection', <-- a dire la verità ho aggiunto anche questo
       'connectionString' => 'mysql:host=jbookdevdb.db.8544297.hostedresource.com;dbname=jbookdevdb',
       'tablePrefix' => 'jb_', <-------------
       'emulatePrepare' => true,
       'username' => 'jbookdevdb',
       'password' => 'Devdbase1',
       'charset' => 'utf8',
 ),

PHPUnit[modifica]

^TOP

Questo package è utilissimo per il test delle classi PHP in un’ottica di unit testing. Lo unit testing è una procedura per verificare il funzionamento di singole parti di codice sorgente (units). Maggiori dettagli qui.

Per usare PHPUnit ho dovuto fare una serie di passaggi propedeutici come l’installazione di PEAR.

Innanzitutto ho acceduto alla consolle come super user:

 $ sudo -s

Ho installato php-pear

 # apt-get install php-pear

Dopo ho scaricato PEAR nella versione 1.9.4:

 root@jsbach:~# pear install PEAR-1.9.4

e poi PHPUnit:

 root@jsbach:~# pear install -a phpunit/PHPUnit

solo che ho incontrato questo problema:

 Unknown remote channel: pear.symfony-project.com

Dunque ho dovuto fare questi passaggi:

 # pear channel-update pear.php.net
    Updating channel "pear.php.net"
    Channel "pear.php.net" is up to date
 # pear upgrade-all
 # pear channel-discover pear.phpunit.de
   Adding Channel "pear.phpunit.de" succeeded
   Discovery of channel "pear.phpunit.de" succeeded
 # pear channel-discover components.ez.no
   Adding Channel "components.ez.no" succeeded
   Discovery of channel "components.ez.no" succeeded
 # pear channel-discover pear.symfony-project.com
   Adding Channel "pear.symfony-project.com" succeeded
   Discovery of channel "pear.symfony-project.com" succeeded

A questo punto potevo installare PHPUnit:

 # pear install -a phpunit/PHPUnit
 ...
 install ok: channel://pear.symfony-project.com/YAML-1.0.6
 install ok: channel://pear.phpunit.de/PHPUnit-3.6.7

Qui attenzione, mi ha installato la 3.6.7 con la quale però ho avuto un problema che sfortunatamente non ho documentato: per risolverlo ho dovuto installare una versione più vecchia (downgrade) di PHPUnit:

 # pear uninstall phpunit/PHPUnit
   uninstall ok: channel://pear.phpunit.de/PHPUnit-3.6.7
 # pear install -a phpunit/PHPUnit-3.4.15

Alla fine potevo lanciare il comando per eseguire lo unit test:

 marcob@jsbach:~/www/trackstar/protected/tests$ phpunit unit/ProjectTest.php

che però falliva dicendomi che non trovava il file MockObject/Autoload.php. Semplicemente il file c’era ma si trovava in un’altra directory, per cui ho pensato alla soluzione più veloce di fare un link simbolico nella directory dove se lo aspettava:

 root@jsbach:/usr/share/php/PHPUnit# ln -s ./Framework/MockObject/Autoload.php .

A questo punto lo unit test ha funzionato:

 marcob@jsbach:~/www/trackstar/protected/tests$ phpunit unit/ProjectTest.php
 PHPUnit 3.4.15 by Sebastian Bergmann.
 
 .
 
 Time: 1 second, Memory: 12.00Mb
 
 OK (1 test, 1 assertion)
 marcob@jsbach:~/www/trackstar/protected/tests$

Esempio di unit test della connessione MySQL[modifica]

Mi capita questo errore

  PHP Fatal error:  Class 'CTestCase' not found in /var/www/html/reminder-yii/protected/tests/unit/DbTest.php on line 4

quando lancio da linea di comando

  $ phpunit tests/unit/DbTest.php

dalla directory

  /var/www/html/reminder-yii/protected/tests

e l'errore è perché lanciamo phpunit in una directory diversa da quella che contiene bootstrap.php; quindi rimango nella directory in cui mi trovo e lancio piuttosto

  $ phpunit unit/DbTest.php

(nota la differenza). Per rendere meno grigio il lavoro, modifico il file phpunit.xml ponendo:

   <phpunit bootstrap="bootstrap.php"
       colors="true"

Test funzionale[modifica]

Consente un test complessivo su tutta l'applicazione, oltre ai singoli test di unità eseguiti classe per classe, metodo per metodo (Agile, pg. 46).

Il fucntional test ha bisogno del server Selenium:

Ho scaricato e avviato il Selenium server java

 $ cd software/java/
 $ wget http://selenium.googlecode.com/files/selenium-server-standalone-2.15.0.jar
 $ java -jar selenium-server-standalone-2.15.0.jar
 ...
 16:16:53.654 INFO - Started org.openqa.jetty.jetty.Server@1827391d

Data l'applicazione trackstar eseguo questo test funzionale:

protected/tests/functional/SiteTest.php

(SiteTest.php c'è già)

Dobbiamo modificare il file protected/tests/WebTestCase.php al fine di dare al server Selenium il corretto URL dell'applicazione da testare.

Apriamo il file e assicuriamoci che la definizione TEST_BASE_URL corrisponda all'URL per l'applicazione demo che abbiamo creato, cioè, cambiare la seguente riga:

  define ('TEST_BASE_URL', 'http://localhost/testdrive/index_test.php / ');

con

  define ('TEST_BASE_URL', 'http: //localhost/demo/index-test.php/');

Poi diciamo a Selenium di utilizzare Firefox per i suoi test (sì! vedremo finestre di Firefox aprirsi e chiudersi da sole su comando di Selenium) modificando il file protected/tests/phpunit.xml in modo che si veda così:

  <phpunit bootstrap="bootstrap.php"
      colors="false"
      convertErrorsToExceptions="true"
      convertNoticesToExceptions="true"
      convertWarningsToExceptions="true"
      stopOnFailure="false">
     <selenium>
          <browser name="Firefox" browser="*firefox" />
     </selenium>
  </phpunit>

Ultimo step, lanciamo il test:

  $ cd protected/tests/
  $ phpunit functional/SiteTest.php

e qui, come al solito c'è un errore, nel mio caso questo

   marcob@jsbach:/var/www/html/trackstar/protected/tests$ phpunit functional/SiteTest.php 
   PHP Warning:  require_once(PHPUnit/Extensions/SeleniumTestCase.php): failed to open stream: No such file or directory in /var/www/html/yii/framework/test/CWebTestCase.php on line 11
   PHP Stack trace:
   PHP   1. {main}() /usr/local/bin/phpunit:0
   PHP   2. PHPUnit_TextUI_Command::main() /usr/local/bin/phpunit:619
   PHP   3. PHPUnit_TextUI_Command->run() phar:///usr/local/bin/phpunit/phpunit/TextUI/Command.php:103
   PHP   4. PHPUnit_TextUI_Command->handleArguments() phar:///usr/local/bin/phpunit/phpunit/TextUI/Command.php:113
   PHP   5. PHPUnit_TextUI_Command->handleBootstrap() phar:///usr/local/bin/phpunit/phpunit/TextUI/Command.php:611
   PHP   6. PHPUnit_Util_Fileloader::checkAndLoad() phar:///usr/local/bin/phpunit/phpunit/TextUI/Command.php:781
   PHP   7. PHPUnit_Util_Fileloader::load() phar:///usr/local/bin/phpunit/phpunit/Util/Fileloader.php:42
   PHP   8. include_once() phar:///usr/local/bin/phpunit/phpunit/Util/Fileloader.php:58
   PHP   9. require_once() /var/www/html/trackstar/protected/tests/bootstrap.php:8
   PHP  10. spl_autoload_call() /var/www/html/trackstar/protected/tests/bootstrap.php:15
   PHP  11. YiiBase::autoload() /var/www/html/trackstar/protected/tests/bootstrap.php:0
   PHP  12. include() /var/www/html/yii/framework/YiiBase.php:402
   PHP Fatal error:  require_once(): Failed opening required 'PHPUnit/Extensions/SeleniumTestCase.php' (include_path='.:/usr/share/php:/usr/local/lib/php') in /var/www/html/yii/framework/test/CWebTestCase.php on line 11
   PHP Stack trace:
   PHP   1. {main}() /usr/local/bin/phpunit:0
   PHP   2. PHPUnit_TextUI_Command::main() /usr/local/bin/phpunit:619
   PHP   3. PHPUnit_TextUI_Command->run() phar:///usr/local/bin/phpunit/phpunit/TextUI/Command.php:103
   PHP   4. PHPUnit_TextUI_Command->handleArguments() phar:///usr/local/bin/phpunit/phpunit/TextUI/Command.php:113
   PHP   5. PHPUnit_TextUI_Command->handleBootstrap() phar:///usr/local/bin/phpunit/phpunit/TextUI/Command.php:611
   PHP   6. PHPUnit_Util_Fileloader::checkAndLoad() phar:///usr/local/bin/phpunit/phpunit/TextUI/Command.php:781
   PHP   7. PHPUnit_Util_Fileloader::load() phar:///usr/local/bin/phpunit/phpunit/Util/Fileloader.php:42
   PHP   8. include_once() phar:///usr/local/bin/phpunit/phpunit/Util/Fileloader.php:58
   PHP   9. require_once() /var/www/html/trackstar/protected/tests/bootstrap.php:8
   PHP  10. spl_autoload_call() /var/www/html/trackstar/protected/tests/bootstrap.php:15
   PHP  11. YiiBase::autoload() /var/www/html/trackstar/protected/tests/bootstrap.php:0
   PHP  12. include() /var/www/html/yii/framework/YiiBase.php:402
   marcob@jsbach:/var/www/html/trackstar/protected/tests$

Qui ci sono due ordini di problemi:

  • devo avere nell'include_path dell'interprete da riga di comando php (/usr/bin/php) il percorso /usr/share/php/ e per fare questo si deve configurare il php.ini (non quello di Apache! - /etc/php5/apache2/php.ini - ma quello della riga di comando: /etc/php5/cli/php.ini) in mdo tale che contenga /usr/share/php/
  • devo avere il file SeleniumTestCase.php sotto la directory /usr/share/php/PHPUnit/Extensions/

In effetti nel mio caso questo file non esiste! Quindi lo creo andando qui e facendo copia incolla del file.

Eseguite queste due operazioni ora rifaccio girare il test

 $ phpunit functional/SiteTest.php 

E il test si avvia e i browser si aprono e si chiudono che è un piacere, e alla fine rimane questo messaggio:

   $ phpunit functional/SiteTest.php 
   PHPUnit 4.4.2 by Sebastian Bergmann.
   
   Configuration read from /var/www/html/trackstar/protected/tests/phpunit.xml
   
   ...
   
   Time: 22.25 seconds, Memory: 5.75Mb
   
   OK (3 tests, 10 assertions)
   $ 
   

Ogni volta che si chiude un browser appare un puntino di quelli in grassetto

Secondo tentativo, stavolta utilizzo Composer[modifica]

Composer è un software per la gestione delle dipendenze dei pacchetti PHP

   # apt-get install curl
   Lettura elenco dei pacchetti... Fatto
   Generazione albero delle dipendenze       
   Lettura informazioni sullo stato... Fatto
   I seguenti pacchetti sono stati installati automaticamente e non sono più richiesti:
     kde-l10n-engb kde-l10n-it linux-image-3.13.0-32-generic
     linux-image-extra-3.13.0-32-generic
   Usare "apt-get autoremove" per rimuoverli.
   I seguenti pacchetti NUOVI saranno installati:
     curl
   0 aggiornati, 1 installati, 0 da rimuovere e 26 non aggiornati.
   È necessario scaricare 123 kB di archivi.
   Dopo quest'operazione, verranno occupati 314 kB di spazio su disco.
   Scaricamento di:1 http://it.archive.ubuntu.com/ubuntu/ trusty-updates/main curl amd64 7.35.0-1ubuntu2.3 [123 kB]
   Recuperati 123 kB in 2s (55,0 kB/s)
   Selezionato il pacchetto curl non precedentemente selezionato.
   (Lettura del database... 304633 file e directory attualmente installati.)
   Preparativi per estrarre .../curl_7.35.0-1ubuntu2.3_amd64.deb...
   Estrazione di curl (7.35.0-1ubuntu2.3)...
   Elaborazione dei trigger per man-db (2.6.7.1-1ubuntu1)...
   Configurazione di curl (7.35.0-1ubuntu2.3)...
   
   root@jsbach:/var/www/html# curl -sS https://getcomposer.org/installer | php
   #!/usr/bin/env php
   All settings correct for using Composer
   Downloading...
   
   Composer successfully installed to: /var/www/html/composer.phar
   Use it: php composer.phar
   
   root@jsbach:/var/www/html# mv composer.phar /usr/local/bin/composer
   

Ora creiamo un file di dipendenza

   root@jsbach:/var/www/html# composer global require "phpunit/phpunit=4.4.*"
   Changed current directory to /home/marcob/.composer
   ./composer.json has been created
   Loading composer repositories with package information
   Updating dependencies (including require-dev)
   
   root@jsbach:/var/www/html# composer global require "phpunit/phpunit=4.4.*"
   Changed current directory to /home/marcob/.composer
   ./composer.json has been created
   Loading composer repositories with package information
   Updating dependencies (including require-dev)
   
     - Installing sebastian/version (1.0.4)
       Downloading: 100%         
   
     - Installing sebastian/global-state (1.0.0)
       Downloading: 100%         
   
     - Installing sebastian/exporter (1.0.2)
       Downloading: 100%         
   
     - Installing sebastian/environment (1.2.1)
       Downloading: 100%         
   
     - Installing sebastian/diff (1.2.0)
       Downloading: 100%         
   
     - Installing sebastian/comparator (1.1.0)
       Downloading: 100%         
   
     - Installing symfony/yaml (v2.6.3)
       Downloading: 100%         
   
     - Installing doctrine/instantiator (1.0.4)
       Downloading: 100%         
   
     - Installing phpunit/php-text-template (1.2.0)
       Downloading: 100%         
   
     - Installing phpunit/phpunit-mock-objects (2.3.0)
       Downloading: 100%         
   
     - Installing phpunit/php-timer (1.0.5)
       Downloading: 100%         
   
     - Installing phpunit/php-file-iterator (1.3.4)
       Downloading: 100%         
   
     - Installing phpunit/php-token-stream (1.4.0)
       Downloading: 100%         
   
     - Installing phpunit/php-code-coverage (2.0.14)
       Downloading: 100%         
   
     - Installing phpunit/phpunit (4.4.2)
       Downloading: 100%         
   
   sebastian/global-state suggests installing ext-uopz (*)
   phpunit/phpunit suggests installing phpunit/php-invoker (~1.1)
   Writing lock file
   Generating autoload files
   # cd /home/marcob
   # nano .composer/composer.json
   {
       "require": {
          "phpunit/phpunit": "4.4.*"

"phpunit/phpunit-selenium": ">=1.2" <--- aggiungo questa

       }
   }

Log e unit test[modifica]

Mi trovo un test di unità che mi ritorna questo errore

  CDbException: CDbCommand ha riportato un errore nell'esecuzione della query SQL: SQLSTATE[23000]: Integrity constraint violation: 
  1452 Cannot add or update a child row: a foreign key constraint fails 
  (`reminder_test`.`reminder`, CONSTRAINT `fk_reminder_payment` FOREIGN KEY (`payment_id`) REFERENCES `payment` (`id`) ON DELETE NO ACTION ON UPDATE NO ACTION)

Il metodo invocato

   protected/tests$ phpunit unit/ReminderTest.php

contiene la parametrizzazione corretta (passo tutti i parametri da inserire nella tabella)

       $newReminder->setAttributes(
           array(
               's_time' => date('Y-m-d', CDateTimeParser::parse('06/03/2015', 'dd/MM/yyyy')),
               'period' => 10,
               'message' => 'Test Reminder 1',
               'period_id' => 1,
               'msgtype_id' => 1,
               'payment_id' => 1,
           )
       );

In particolare payment_id è = 1 e corrisponde ad una riga effettivamente esistente (non c'è violazione di chiave esterna). Ma quale sarà l'istruzione SQL che effettivamente viene lanciata dal controller? Devo attivare il debug.

Apro protected/config/main.php e trovo

       'log'=>array(
           'class'=>'CLogRouter',
           'routes'=>array(
               array(
                   'class'=>'CFileLogRoute',
                   'levels'=>'error, warning',
               ),
               /*uncomment the following to show log messages on web pages*/ 
               /*
               array(
                   'class'=>'CWebLogRoute',
                   'levels'=>'trace, info, error, warning',
               ),
               */
           ),
       ),

Non mi serve scommentare perché comunque non vedo l'output da web (sto usando la shell per gli unit test), però modifico così:

           'routes'=>array(
               array(
                   'class'=>'CFileLogRoute',
                   'levels'=>'trace, info, error, warning',
                   'categories' => 'system.db.CDbCommand',
                   'logFile' => 'db.log',
               ),


Con levels (trace, info) aggiungo un logging più prolisso, mentre con categories mi concentro soltanto gli errori provenienti dal database. Il file in cui deposito i log è db.log.

Seconda istruzione da controllare, sempre nello stesso file:

   'preload'=>array('log'),

c'è già, mi sono solo sincerato che non fosse commentata.

Non è finita, devo aggiungere questa istruzione

   Yii::app()->log->processLogs(null);

al file protected/test/unit/ReminderTest.php

Alla fine dovrei trovare il risultato dei log qui: protected/runtime/application.log. In realtà non vedo nessun file protected/runtime/db.log

Però... se attivo anche la sezione web nel main.php (scommento 'class'=>'CWebLogRoute') allora il file si crea se provo l'applicazione da web.

Il logging su file da unit test funziona SOLO DOPO che ho fatto questa (per me) misteriosa modifica al file ReminderTest.php

  public static function tearDownAfterClass()
  {
      Yii::app()->onEndRequest(new CEvent(null));
  }

Ho in sostanza aggiunto questo metodo alla classe

  class ReminderTest extends CDbTestCase 

definita in ReminderTest.php.

E come per magia, dopo aver lanciato da shell

  $ phpunit unit/ReminderTest.php

è comparso il file

   marcob@jsbach:/var/www/html/reminder-yii/protected/runtime$ ll
   totale 276
   drwxrwxr-x  3 www-data marcob   4096 mar  6 12:04 ./
   drwxrwxr-x 14 www-data marcob   4096 ago 31  2014 ../
   -rwxrwxr-x  1 www-data marcob 256585 mar  5 19:37 application.log*
   -rw-rw-r--  1 marcob   marcob    532 mar  6 12:04 db.log  <----------
   drwxrwxr-x  2 www-data marcob   4096 feb  5 19:49 gii-1.1.8/
   -rwxrwxr-x  1 www-data marcob     88 ott  9 11:32 state.bin*

il cui contenuto è

   2015/03/06 12:04:43 [error] [system.db.CDbCommand] CDbCommand::execute() failed: SQLSTATE[23000]: Integrity constraint violation: 
   1452 Cannot add or update a child row: a foreign key constraint fails (`reminder_test`.`reminder`, CONSTRAINT `fk_reminder_payment` 
   FOREIGN KEY (`payment_id`) REFERENCES `payment` (`id`) ON DELETE NO ACTION ON UPDATE NO ACTION). 
   The SQL statement executed was: INSERT INTO `reminder` (`period`, `s_time`, `message`, `period_id`, `msgtype_id`, `payment_id`) 
   VALUES (:yp0, :yp1, :yp2, :yp3, :yp4, :yp5).

Bene. Però non vedo il valore dei parametri :-( Sempre nella stessa risorsa (http://www.yiiframework.com) trovo la risposta: occorre passare un parametro alla configurazione del database (protected/condig/main.php):

       'db'=>array(
           'connectionString' => 'mysql:host=localhost;dbname=reminder',
           'enableParamLogging' => true, <----------------
           'emulatePrepare' => true,
           'username' => 'root',
           'password' => 'root01',
           'charset' => 'utf8',
       ),

L'output su file cambia così:

   2015/03/06 12:13:32 [error] [system.db.CDbCommand] CDbCommand::execute() failed: SQLSTATE[23000]: Integrity constraint violation: 
   1452 Cannot add or update a child row: a foreign key constraint fails (`reminder_test`.`reminder`, CONSTRAINT `fk_reminder_payment` 
   FOREIGN KEY (`payment_id`) REFERENCES `payment` (`id`) ON DELETE NO ACTION ON UPDATE NO ACTION). 
   The SQL statement executed was: INSERT INTO `reminder` (`period`, `s_time`, `message`, `period_id`, `msgtype_id`, `payment_id`) 
   VALUES (:yp0, :yp1, :yp2, :yp3, :yp4, :yp5). 
   Bound with :yp0=10, :yp1='1970-01-01 01:00:00', :yp2='Test Reminder 1', :yp3=1, :yp4=1, :yp5=1.

Bene. Alla fine l'istruione non ha alcun problema, è costruita correttamente e, sostituiti i segnaposto e lanciata la query dal MySQL Benchmark, l'insert funziona benissimo. Quindi?

Quindi...[modifica]

Fare attenzione: se seguite la best practice di usare un database di TEST diverso dal database di SVILUPPO, accertarsi di guardare il database giusto!

Una cosa tutto questo me l'ha insegnata: come abilitare il logging su browser e su filesystem in Yii.

Dalla _REQUEST alla pagina web[modifica]

Grande è la tentazione di utilizzare i valori provenienti dalla request direttamente nella view, ma così facendo rischiamo di perdere pezzi per strada.

Il percorso corretto da compiere è questo

  $_REQUEST ---> controller ---> action ---> view.

Esempio, voglio far transitare una variabile $payment_id dal flusso HTTP alla pagina web

  index.php?r=voucher/create&payment_id=2

Step 1: dalla request al controller[modifica]

Supponiamo di voler creare un nuovo record per l'oggetto voucher, che ha bisogno di essere agganciato al record payment_id=1

  • file VoucherController.php
   public function actionCreate()
   {
       $model=new Voucher;
       if(isset($_REQUEST['Voucher']))
       {
           $model->attributes=$_REQUEST['Voucher'];
           $payment_id = $model->payment_id;
           if($model->save())
               $this->redirect(array('index','id'=>$model->id, 'payment_id' => $payment_id));
       }
       else 
       {
           $payment_id = $_REQUEST['payment_id'];
       }
       $this->render('create',array(
           'model'=>$model,
           'payment_id' => $payment_id,
       ));
   }

con questo frammento valorizziamo la variabile da passare e la passiamo al metodo create nell'array dei parametri (il secondo parametro della funzione render).

Step 2: dal controller alla action[modifica]

  • file voucher/create.php
   <?php echo $this->renderPartial('_form', array('model'=>$model, 'payment_id' => $payment_id)); >?>

il valore $payment_id è quello che arriva dalla funzione render citata sopra. Con questa istruzione passiamo al template _form.php l'array che contiene $model e $payment_id

Step 3: dalla action alla view[modifica]

  • file voucher/_form.php
   <?php echo $form->hiddenField($model,'payment_id', array('value' => $payment_id)); ?>

questo è l'ultimo passaggio, siamo arrivati. L'html generato al volo conterrà questo campo hidden:

   <input id="Voucher_payment_id" type="hidden" name="Voucher[payment_id]" value="2">.

Validatori[modifica]

^TOP

Se aggiungo un vincolo ad un campo di una tabella, in un secondo momento, sul db già creato e con le classi già create, posso aggiungere il controllo del vincolo nel file models/NomeClasse.php, al metodo rules.

Ad esempio, il frammento di file seguente (appname/protected/models/Project.php) implementa i controlli per alcuni campi del form:

 class Project extends CActiveRecord
 {
 ...
   public function rules()
   {
       return array(
           array('create_time, name', 'required'),
           array('create_user_id, update_user_id', 'numerical', 'integerOnly'=>true),
 ...

Nell’esempio precedente, all’interno del metodo Project.rules() viene definito l’array dei controlli da applicare; i controlli si configurano come array di array: a livello più alto ogni componente è un controllo; a livello più basso, ogni controllo è a sua volta un array che può essere costituito da due o più componenti; nell’esempio, il primo controllo predispone che i campi create_time e name siano cambi obbligatori (requested), il secondo controllo dice che i campi create/update_user_id siano numerici (‘numerical’) e, ancorché numerici, strettamente interi (‘integerOnly’ => true).

I valori che ho scritto tra parentesi sono i tipi di controllo o tipi di validatore. Ci sono molti tipi di validatori semplici e complessi predefiniti in Yii (che controllano, ad esempio, che un campo sia un intero, che sia un URL ben formato, che sia un captcha).

Oppure si possono aggiungere altri validatori customizzati in due modi alternativi ed equivalenti:

  • aggiungendo un metodoValidator alla classe oppure
  • creando una nuova classeValidator che eredita da CValidator.

Gestione relazioni uno a molti[modifica]

^TOP

Con questo capitolo impariamo a gestire cose tipo: display della maschera dell'utente (User) a cui posso assegnare un (solo) ruolo (Role) che scelgo da una dropdown list.

1) alla view dello User (protected/views/user/_form.php) tolgo l'istruzione

  echo $form->textField($model,'role_id');

e lo sostituisco con

  echo $form->dropDownList($model,'role_id', $model->getRoles());

2) ho invocato un metodo del modello che ancora non esiste, getRoles: lo aggiungo a protected/models/User.php

   public function getRoles()
   { 
     return CHtml::listData(Role::model()->findAll(), 'id', 'description');
   }

mi viene in soccorso il metodo findAll che il mio modello eredita dalla classe madre CActiveRecord

3) basta. O se si vuole si premette a tutto ciò lo unit test

  class UserTest extends CTestCase
  {
       public function testGetRoles()
       {
               $options = User::model()->getRoles();
               $this->assertTrue(is_array($options));
       }
  }

dove anche qui uso il metodo definito nel modello. E' semplicissimo ma ci ho perso un pomeriggio.

Gestione delle relazioni molti-a-molti.[modifica]

^TOP

Con Yii lo possiamo fare senza scrivere una riga di SQL, ma handle with care!

Sto rifacendo l’esempio del Manuale Agile Web Application Development with Yii 1.1 and PHP 5 e ho trovato un particolare non corretto.

In breve l’esempio è l’applicazione Trackstar che è un rudimentale sistema di gestione di ticket. Le entità fondamentali sono i Project (progetti), Issue (segnalazioni) e User (utenti); Issue e User sono collegati da una relazione molti-a-molti.

La situazione è la seguente: sono in una specifica Issue il cui progetto padre è Project1; Project 1 è gestito dagli utenti 1 e 2. Obiettivo: voglio una tendina nella Issue per visualizzare gli utenti 1 e 2. Passaggi: Dall’IssueId devo procurarmi

  • ProjectId
  • Id, e Nome degli utenti collegati al ProjectId calcolato, per costruire il menu a tendina.

Nel controller IssueController.php ho questa istruzione per il calcolo del menu a tendina:

 $this->getProject()->getUserOptions())

ProjectId lo calcolo “affogando” $this->getProject() in

 views/issue/_form.php,

essendo getProject() un metodo di IssueController.php; la variabile privata _project viene inizializzata quando creo il controller, infatti all’inizio ho la funzione loadProject che inizializza _project.

Attenzione: a questo stadio non ho alcun Id Progetto, è il filter (che è un pezzo di programa che va in esecuzione subito prima di invocare il Controller, come un trigger), che inizializza correttamente l’Id in base alla request HTTP.

Il secondo pezzo (Id, Nome degli utenti collegati al ProjectId calcolato == getUserOptions()) è un metodo del model che passa all’helper CHtml::listData l’array con id e descrizione degli elementi da fa comprarire nella tendina; l’array è $this->users all’interno del file models/Project.php il quale viene popolato all’atto della istanziazione dell’oggetto Project: quando viene chiamato il costruttore, la classe madre (CActiveRecord, dalla quale proviene il mio modello Project) manda in esecuzione il metodo relations(), che nella classe genitore è vuoto, ma viene sovrascritto (overridden) dal metodo relations della classe figlia (Project). Questo metodo va scritto in base a come è fatto il database: nel mio caso ho una relazione che mi lega Project a Issue e User a Project; la parte più intereesante è quella molti a molti in cui inizializzo la proprietà ‘users’:

 'users' => array(self::MANY_MANY,
    'User',
    'project_user_assignment(project_id, user_id)'
 ),

Attenzione, punto cruciale: nel voler listare le issues appartenenti ad un project, se opero come da manuale Yii (pg 129), vado incontro ad un problema: nel listato proposto nel manuale invoco la funzione loadModel di Project senza argomenti, mentre, per come è definita in controllers/ProjectController.php essa ha bisogno di un parametro obbligatorio: l’$id del progetto. Ne risulta un errore del server che evidenzia questo problema. Nell’ambito di questa maschera si può risolvere il problema modificando così la definizione del metodo:

 public function loadModel($id = null)
 {
   $id = (!isset($id)) ? $_REQUEST['id'] : $id;
   $model=Project::model()->findByPk($id);
   if($model===null)
     throw new CHttpException(404,'The requested page does not exist.');
   return $model;
 }

dando una possibilità di avere $id null e passando al metodo findByPk, anziché il parametro, il valore calcolato in base alla presenza del parametro di ingresso o in alternativa dalla REQUEST.

Altro problema con il display etichette[modifica]

non funziona il test a pg. 132, però l'applicazione funziona e da quello che sci si aspetta

Indagare su questo problema che è accaduto quando ho modificato i file models/Issue.php aggiungendo getStatusText() e getTypeText() e tests/units/IssueTest.php aggiungendo testGetStatusText() e testGetTypeText() per fare uscire le etichette al posto degli ID

 .PHP Fatal error:  Call to undefined method IssueTest::issues() in /var/www/trackstar/protected/tests/unit/IssueTest.php on line 24

[Forum Yii | http://www.yiiframework.com/forum/index.php?/topic/11954-chapter-6-undefined-method-issuetestissues/]

Roles, Tasks, Operations[modifica]

  • owner (ruolo)
    • user management (compito)
      • create new user (operazione)
      • edit user (operazione)
      • delete user (operazione)
    • issue management (compito)

Tabella di assegnazione ruoli ad utenti e progetti[modifica]

 create table project_user_role
 (
   project_id INTEGER NOT NULL,
   `user_id` INTEGER NOT NULL,
   role VARCHAR(64) NOT NULL,
   primary key (project_id,`user_id`,role),
   foreign key (project_id) references project (id),
   foreign key (`user_id`) references `user` (id),
   foreign key (role) references AuthItem (name)
 );

Errore nel manuale a pag. 193[modifica]

  public function associateUserToRole($role, $userId)
  {
  $sql = "
  INSERT INTO project_user_role 
  	(project_id, user_id, role) 
  VALUES 
  	(:projectId, :userId, :role)";
  
  $command = Yii::app()->db->createCommand($sql);
  
  $command->bindValue(":projectId", $this->id, PDO::PARAM_INT);
  $command->bindValue(":userId", $userId, PDO::PARAM_INT);
  $command->bindValue(":role", $role, PDO::PARAM_STR);
  
  return $command->execute(); <--- nel libro non c'è ()
  }
  

Mettere mano al menu[modifica]

Aprire il file /views/layouts/main.php

Ad esempio aggiungiamo la voce Clienti: