Uwaga, blog przeniesiony

Posty na tym blogu już nie będą się pojawiać. Zapraszam gorąco pod nowy adres: blog.grzegorzpawlik.com



Subskrybuj ten blog...

środa, 25 marca 2009

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 ;))

wtorek, 24 marca 2009

CakeFest o rzut beretem


Jeśli masz niedaleko i cake to Twoje środowisko pracy - nie wahaj się ani chwili ;)

wtorek, 17 marca 2009

Przyspieszyć cake 1.1.x

Przy okazji usprawnień dla portalu dobrzemieszkaj.pl i zabawy z xdebug.profiler udało mi się znaleźć ciekawe wąskie gardło w cakePHP.
Niestety jego usunięcie wymaga ingerencji w core frameworku, ale z uwagi na to, że wersja 1.1 nie będzie już rozwijana, a do tego przy tak zaawansowanym poziomie prac nad portalem na pewno nie będziemy migrować do żadnej innej nowiści - uważam, że można było sobie pozwolić na grzebanie w core.

O co chodzi?
Konstruktor modelu wywołuje metodę Model::setSource(), która przede wszystkim sprawdza, czy wśród wszystkich tabel jest ta, z którą powiązany jest dany model. W portalu na stronie głównej było ok 50 wywołań konstruktorów modeli, a co za tym idzie- również Model::serSource.

Wszystko byłoby w porządku, gdyby nie to, że ta funkcja zabezpiecza się przed przypadkiem niestosowania się w 100% do konwensji cake. Dlatego wszystkie elementy (nazwy tabel) traktowała funckją strtolow().

Z uwagi na to, że w naszym projekcie jest 150 tabel, to przebiegnięcie po tablicy 150 stringów i zamiana na lowercase odbywała się 50 razy, czy razem było  750 stringów było zamienianych na lowercase. Sęk w tym, że operacja nie była potrzebna, gdyż wszystkie nasze tabele są lowercase.
Dlatego dobrą opcją było zamiana cake/libs/model/model_php5.php:523 z
if (is_array($sources) && !in_array(low($prefix . $tableName), array_map('low', $sources))) {
na
if (is_array($sources) && !in_array($prefix . $tableName,  $sources)) {
proste jak budowa cepa, a ubyło kolejne ćwierć sekundy na generowanie strony :)

W prawdzie nie jest to oszołamiające przyspieszenie, ale przy bardzo wielu zapytaniach na raz - przydatne.

Edit: członkini teamu cake Jitka Koukalova potwierdziła, że ten fragment kodu jest obejściem, dzięki któremu cake działa również z php4. Twierdzi też, że to jeden z wielu elementów, bez których cakePHP będzie działał szybciej, ale tylko na php5.

You discovered one of many workarounds which are necessary for PHP4.
It is just one of many things which will make PHP5 only CakePHP much
faster.
Nic tylko dalej szukać i usprawniać ;)

piątek, 13 marca 2009

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:


<?php
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:



<?php
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 ;)


opisać problem z coverage, jeśli grupa jest zdefiniowana jako loadTestCasesFromFile

środa, 4 marca 2009

W tej branży 2+2 nie równa się 4

Ten problem nakreślił niedawno Patrys na blogu, który czytuję (http://room-303.com/blog/2009/02/20/przypowiesc-o-osobomiesiacu/) Mam ochotę napisać o tym nieco więcej.

Ciągle mam problem ze złym podejściem do produktu jakim jest oprogramowanie. Dlatego będę pisał o tym pewnie za każdym razem, gdy da mi się we znaki. Jak zwykle zpróbuję opisać to na przykładzie.

Załóżmy, że pytasz mnie ile czasu pracy jednego programisty zajmnie gdyby miał zaprogramować:

- Prosty blog
 Ja odpowiem, że dwa miesiące.
- Galerię zdjęć
Ja również odpowiem, że dwa miesiące.

Jeśli uważasz, że to wystarczające szacowania i gdybyś chciał dostać system, który ma funkcjonalności kryjące się pod hasłem "Prosty blog" i "Galeria zdjęć", oszacujesz już sobie sam, że to razem 4 miesiące...

Otóż jesteś w błędzie. Ten nowy system zajmie więcej czasu.

Jeśli uważasz, że przypisując czterech programistów do tego projektu czas jego wykonania skróci się czterokrotnie - również się pomyliłeś. Zapomniałeś o tym, że nie zawsze można podzielić projekt na kilka niezależnych elementów, które można implementować współbierznie. Nawet gdyby się dało - ktoś musi odwalić tą robotę, odpowiednio zaprojektować i koordynować współbierzną implementację.

Dlatego jeśli jesteś menadżerem, kierownikiem projektu, sponsorem lub klientem - oddasz przysługę wielu ludziom, jeśli przestaniesz traktować tworzenie oprogramowania jak skręcanie długopisów.
Nam zaoszczędzisz nerwów i wrzodów na żałądku, a sobie - rozczarowań.

wtorek, 3 marca 2009

ImageBehavior - uploaduj pliki prosto do bazy

Jakiś czas temu napisałem o pomyśle cake'owego Behavior (http://webbricks.blogspot.com/2009/02/pliki-w-formie-binarnej-w-bazie.html). Poniżej prezentuję pierwsze podejście do problemu:

<?php
/**
 * ImageBehavior - take best from database blobs adn file image storage

 * requires 'content' field that is a blob (mediumblob or longblob), and
 * 'ext' varchar(10) field  and

 * 'modified' datetime field
 * @author Grzegorz Pawlik
 * @version 1.0
 */
class ImageBehavior extends ModelBehavior {

  
   
/**
    * directory in which cached files will be stored
    *
    * @var string
    */
   
var $cacheSubdir 'filecache';

   
/**
    * if set to false - never check if cached file is present (nor actual)
    *

    * @var bool
    */
   
var $usecache true;
  
   function 
setup(&$Model) {
      
// no setup at this time

   
}
  
   
/**
    * Insert proper blob when standard data after upload is present
    *
    * @param object $Model

    * @return bool true
    */
   
function beforeSave(&$Model) {

      if(isset(
$Model->data[$Model->name]['file']['tmp_name']) && is_uploaded_file($Model->data[$Model->name]['file']['tmp_name'])) {

      
// podnieś wyżej parametry
      
$Model->data[$Model->name] = array_merge($Model->data[$Model->name],  $Model->data[$Model->name]['file']);

      
// przygotuj blob
      
$this->_prepareBlob($Model);
     
      
$this->_getExt($Model);
      }

     
      return 
true;
   }
  
   
/**
    * prepares blob contents
    *
    * @param object $Model

    */
   
function _prepareBlob(&$Model) {
      
App::import('Core''File');
      
$file = new File($Model->data['Medium']['tmp_name'], false);

      
$content $this->addSlashes$file->read() );
      
$Model->data[$Model->name]['content'] = $content;

   }
  
   
/**
    * Get uploaded file extension
    *
    * @param object $Model
    */
   
function _getExt(&$Model) {

      
$file explode('.'$Model->data['Medium']['name']);
      
$ext array_pop($file);

      
$Model->data[$Model->name]['ext'] = $ext;
   }
  
   
/**
    * replace blob contents with file path

    * After reading database checks if cached file is present. If not creates it (from blob contents) and

    * returns a 'file' field with path relative to /app/webroot/img
    *
    *
    * @param object $model

    * @param array $results
    * @param unknown_type $primary
    * @return unknown
    */
   
function afterFind(&$model$results$primary) {

      foreach(
$results as $key => $val) {
        
        
        
         
$relpath $this->cacheSubdir DS .

                 
$val[$model->name]['id'] . '_' $model->name '_' .

                 
$val[$model->name]['modified'] . '.' $val[$model->name]['ext'];

         
$relpath str_replace( array(' '':') , '_'$relpath);
        
         
$fullpath IMAGES $relpath;

        
         if(!
file_exists($fullpath) || !$this->usecache ) {
            
file_put_contents($fullpath$this->stripSlashes($results[$key][$model->name]['content']));

         }
        
         
$results[$key][$model->name]['file'] = $relpath;
         
// remove blob from results (its messy when You want to output results in debug)

         
unset($results[$key][$model->name]['content']);
      }
      return 
$results;
   }
  

   
/**
    * add slashes (just wrapper)
    *
    * @param string $string
    * @return string with slashes
    */

   
function addSlashes($string) {
      return 
addslashes($string);
   }
  
   
/**
    * strip slashes (just wrapper)

    *
    * @param string $string
    * @return string without slashes
    */
   
function stripSlashes($string) {

      return 
stripslashes($string);
   }
}
?>


Zasada działania jest dość prosta. Wyjaśnię ją na przykładzie.
Tabela media:


CREATE TABLE IF NOT EXISTS `media` (
  `id` int(11) NOT NULL auto_increment,
  `name` varchar(50) NOT NULL,

  `ext` varchar(10) NOT NULL,
  `content` longblob NOT NULL,
  `size` int(11) NOT NULL,
  `created` datetime NOT NULL,
  `modified` datetime NOT NULL,

  `type` varchar(20) NOT NULL,
  PRIMARY KEY  (`id`)
) ENGINE=MyISAM;


Model:
<?php
class Medium extends AppModel {

    var 
$name 'Medium';
   var 
$actsAs = array('Image');

}

?>


Kontroler:
<?php

class MediaController extends AppController {

    var 
$name 'Media';
    var 
$helpers = array('Html''Form');

   
    function 
index() {
     
      
$this->set('media'$this->Medium->findAll());

     
    }
   
    function 
add() {
       if(!empty(
$this->data)) {
          
$this->Medium->save($this->data);

       }
    }
   
}

?>

Przy uploadzie ImageBehavior oczekuje, że plik będzie przekazany w poly ModelName.file (tutaj Media.file).

add.ctp:
span style="color: rgb(0, 0, 187);"><?php
   echo $form->create(
      array(
'url' => array(
                           
'controller' => 'media',

                           
'action'    => 'add'
                     
),
            
'enctype' => 'multipart/form-data'
            
)
   );
?>

<?php echo $form->file('Medium.file'); ?>


<?php echo $form->end('submit'); ?>



Przy odczycie dzieje się to co mnie najbardziej interesowało. Zamiast dostać zawartość (BLOB) pliku, dostajemy w polu file ścieżkę (relatywną do app/webroot/img). Domyślne ustawienia wymagają, żeby był tam katalog filecache (z możliwością zapisu). Przy operacji read behavior sprawdzi, czy istnieje aktualy plik w filecache, i jesli nie - utworzy go.

index.ctp:
<?php foreach($media as $medium): ?>
   <?php echo $html->image($medium['Medium']['file']); ?>

<?php 
endforeach; ?>


To rozwiązanie ma przynajmniej dwa zauważalne braki:
  1. Gdy dodamy taki plik do treści np. postu, po jego updacie - nie będą widoczne zmiany
  2. Dobrze byłoby, gdyby przy operacji read nie zwracał zawartości BLOB (ważne, gdy baza jest gdzieś dalej), ale odpytywał tylko wtedy, gdy jest potrzebne zaktualizowanie zawartości pliku w filecache.
To rzeczy do zrobienia w kolejnym podejściu ;)

Uwaga! blog przeniesiony

Posty na tym blogu już nie będą się pojawiać. Zapraszam gorąco pod nowy adres: blog.grzegorzpawlik.com
Komentowanie artykułów możliwe jest pod nowym adresem.