Skróć swoje listingi

Pewnie w niemal każdym swoim widoku index.ctp masz fragment kodu podobny do tego:

<?php
$i = 0;
foreach ($tracks as $track):
	$class = null;
	if ($i++ % 2 == 0) {
		$class = ' class="altrow"';
	}
?>
	<tr<?php echo $class;?>>
		<td>
			<?php echo $track['Track']['id']; ?>
		</td>
		<td>
			<?php echo $track['Track']['name']; ?>
		</td>
		<td>
			<?php echo $track['Car']['name']; ?>
		</td>
		<td>
			<?php echo $track['Track']['begin']; ?>
		</td>
		<td>
			<?php echo $track['Track']['end']; ?>
		</td>
		<td>
			<?php echo $track['Track']['length']; ?>
		</td>
		<td class="actions">
			<?php echo $html->link(
			   __('Edycja', true),
			   array(
			      'action' => 'edit',
			      $track['Track']['id'] ,
			      $track['Car']['id']
			   )
			  );
			?>

		</td>
	</tr>
<?php endforeach; ?>
</table>

Chce Ci się pisać ten kod kilkadziesiąt razy w jednym projekcie? Mnie też nie. Dlatego napisałem prosty element, który to trochę automatyzuje (nazwałem go “list”):

// app/views/elements/list.ctp
<?php
   $i = 0;
   foreach($elements as $key => $element):
   $class = null;
   if ($i++ % 2 == 0) {
      $class = ' class="altrow"';
   }
?>
   <tr <?php echo $class?>>
      <?php foreach($fields as $path): ?>
         <td>
            <?php echo Set::classicExtract($element, $path) ?>
         </td>
      <?php endforeach; ?>
         <?php if(isset($links)): ?>
            <td>
               <?php foreach($links as $name => $url): ?>
                  <?php
                     if(!is_array($url["params"])) {
                        $url["params"] = array($url["params"]);
                     }
                     foreach($url["params"] as $key => $path){
                        $url[$key] = Set::classicExtract(
                           $element,
                           $path
                        );
                     }
                     unset($url["params"]);
                  ?>
                  <?php echo $html->link($name, $url)?>
               <?php endforeach; ?>
            </td>
         <?php endif; ?>
   </tr>
<?php endforeach; ?>

Jego użycie jest dość proste. Dla przykładu wygenerujemy ten sam “output” dla pierwszego listingu z tego postu:

echo $this->renderElement(
         "list",
         array(
            "elements" => $tracks,
            "fields" => array(
               "Track.name", "Car.name", "Track.begin",
               "Track.end", "Track.length"
            ),
            "links"=> array(
               "edycja" => array(
                  "controller"=> "tracks", "action"=> "edit",
                  "params" => array(
                     "Track.id", "Car.id"
                  )
               )
            )
         )
      )

Opiszę poszczególne parametry przekazywane do tego elementu:

  • “elements” – kolekcja (w tym wypadku tablica) elementów zwrócona na przykład przez $this->Track->find(“all”)
  • “fields” – pola, które mają się pojawić w komórkach tabeli
  • “links” – tu definiujemy jakie linki mają się pojawić w ostatniej kolumnie (zazwyczaj tam znajdują się “opcje”). Indeksy kolejnych elementów to nazwy linków (tutaj “edycja”).
    • “controller” i “action” są identycznie używane jak w metodzie HtmlHelper::link()
    • “params” jest dość ciekawy. Jeśli chcemy w wygenerowanym linku mieć jeden parametr (najczęściej id), robimy to tak:
                  "edycja" => array(
                     "controller"=> "cars", "action"=> "edit", "params"=>"Car.id"
                  ),
      

      gdy parametrów ma być więcej – ścieżki do nich przekazujemy w tablicy (tak jak w przykładzie):

                      "edycja" => array(  
                         "controller"=> "tracks", "action"=> "edit",  
                         "params" => array(  
                            "Track.id", "Car.id"  
                         )  
                      ) 
      

      Można także używać parametrów typu “named”, wystarczy podać dodatkowo indeksy:

                      "edycja" => array(  
                         "controller"=> "tracks", "action"=> "edit",  
                         "params" => array(  
                           "parametr_1"=> "Track.id", "parametr_2"=> "Car.id"  
                         )  
                      ) 
      

Zachęcam do korzystania z tego elementu i zgłaszania wszelkich niedociągnięć :)

Share Button

Zarządzanie wersjami STRUKTURY bazy danych w cakePHP 1.2

W poprzednich postach (m.in. zarządzanie wersjami oprogramowania) udało mi się nakreślić problem przy zarządzaniu oprogramowaniem pojawiający się na styku kod-baza danych. Nawet mogę powiedzieć, że mały sukces na tym polu odnotowałem przy pomocy ImageBehavior, jednak jeśli chodzi o strukturę – ciągle zmagałem się do tej pory z przeciwnościami.

Jednak okazuje się, że cake w nowym wydaniu wychodzi nam na przeciw razem z klasą Schema, oraz z narzędziem konsolowym ./cake schema … po krótce opowiem o co chodzi.

Zabawę z tym narzędziem najlepiej zacząć mając już jakiś zalążek aplikacji (tabele + modele). Jeśli sprawiamy ten podstawowy warunek możemy wpisać w konsoli ./cake schema generate … ot tak, dla jaj.

Następnie możemy się w katalogu app/config/sql/ namierzyć plik schema.php. To właśnie artefakt wygenerowany przez nas przed sekundą. Można w celach samorozwojowych zajrzeć do środka…

Jednak ciekawe rzeczy dzieją się, kiedy ponownie wywołamy to samo polecenie: otóż cake rezolutnie zauważy, że plik schema.php już istnieje i zapyta nas co dalej. Polecam wybór opcji [S]napshot i ponowny rzut oka do wspomnianego wyżej katalogu. Co widzimy? Dokładnie! Nowy plik o nazwie schema_2.php :D Zachęcam do zapoznania się z helpem (./cake schema help).

Wystarczy, że teraz przekonam zespół, aby w sytuacji, gdy nastąpiły zmiany w bazie, przed commitem wywołali to polecenie. Jest jeden problem, którego ewentualnie można się spodziewać – sporadycznych konfliktów. To znaczy sytuacji, w której dwóch programistów:

  1. ściąga repozytorium, 
  2. dokonuje (nawet różnych) zmian w bazie, 
  3. zatwierdza dane: 
    1. schema generate, 
    2. svn add schema_X.php, 
    3. svn commit

Problem w tym, że w takiej sytuacji w punkcie 3.3 jeden z nich dostanie informację

Nie mogę dodać schema_X.php do repozytorium, gdyż takowy  już w repozytorium istnieje.
Z poważaniem Twój
SVN

Nie jest to jakaś wielka tragedia, jak przy każdym konflikcie trzeba będzie go rozwiązać (w tym wypadku przy spotkaniu tych dwóch programistów). Jednak myślę, że takie sytuacje można by zlikwidować wywołując tą sekwencję w jednym ciągu (nie np. commit po dwóch godzinach od schema generate), może nawet napisać prosty skrypt, który załatwi to za nas (taki svncommitwithcakeschemagenerate.sh ;))

Share Button

CakeTestSuite i pokrycie kodu (code coverage)

Nareszcie działa ;) Okazuje się, że zmuszenie powyższego do poprawnego działania za pomocą dostępnych oficjalnych opisów nie jest takie proste.

Np. metoda podana pod http://bakery.cakephp.org/articles/view/testing-models-with-cakephp-1-2-test-suite dla testu modelu się nie sprawdza. Okazuje się, że tam jest podany stary model tworzenia testów. Poprawna klasa powinna wyglądać mniej więcej tak:


App::import('model', 'Article');
class ArticleTestCase extends CakeTestCase {
var $fixtures = array( 'app.article' );

function start() {
parent::start();
$this->Article= & ClassRegistry::init('article');
}

function testPublished() {
$result = $this->Article->published(array('id', 'title'));
$expected = array(
array('Article' => array( 'id' => 1, 'title' => 'First Article' )),
array('Article' => array( 'id' => 2, 'title' => 'Second Article' )),
array('Article' => array( 'id' => 3, 'title' => 'Third Article' ))
);
$this->assertEqual($result, $expected);
}
}

zamiast podanej:

loadModel('Article');
class ArticleTest extends Article {
var $name = 'ArticleTest';
var $useDbConfig = 'test_suite';
}
class ArticleTestCase extends CakeTestCase {
var $fixtures = array( 'article_test' );
function testPublished() {
$this->ArticleTest =& new ArticleTest();
$result = $this->ArticleTest->published(array('id', 'title'));
$expected = array(
array('ArticleTest' => array( 'id' => 1, 'title' => 'First Article' )),
array('ArticleTest' => array( 'id' => 2, 'title' => 'Second Article' )),
array('ArticleTest' => array( 'id' => 3, 'title' => 'Third Article' ))
);
$this->assertEqual($result, $expected);
}
}

Drugi przypadek generuje następujące problemy:

  1. Gdy testy masz ustawione tak, żeby korzystały z tej samej bazy, gdzie masz “normalne” tabele, to testy na zmianę będą wykonywać się poprawnie i zgłaszać błąd z nieistniejącą tabelą (`articles_test`)
  2. Gdy masz osobną baze do testów – będą się sypać relacje. Pewnie zdefiniowanie wszystkich fixtures powiązanych z testowanym modelem rozwiązało by problem, ale nie sprawdzałem.

Kolejnym przypadkiem jest zmuszenie CakeTestSuite do wywalenia informacji o procentowym pokryciu kodu testami. Pominę problemy przy instalacji xdebug, założę, że już to masz za sobą.

Prawdopodobnie będziesz dostawał Segmentation Fault po kliknięciu “Analyze Code Coverage”. Jeśli tak, odnajdź poniższą linię w pliku cake/test/lib/code_coverage_manager.php:

xdebug_start_code_coverage(XDEBUG_CC_UNUSED | XDEBUG_CC_DEAD_CODE);

Niestety Xdebug sypie się przy takim wykonaniu xdebug_start_code_coverage. Możesz zmienić tą linię na

xdebug_start_code_coverage(XDEBUG_CC_UNUSED)

lub

xdebug_start_code_coverage(XDEBUG_CC_DEAD_CODE)

O rożnicy możesz poczytać w dokumentacji Xdebug.

Ostatnia sprawa to pokrycie kodu, gdy wykonujesz test całej grupy. Jeśli chcesz dostawać informacje o pokryciu dla poszczególnych testów, nie używaj TestManager::loadTestCasesFromFile, ale raczej ładuj każdy plik osobno za pomocą TestManager::addTestFile.

To na razie tyle. Miłego testowania ;)

Share Button