Nowe odkrycie – Phing

Zawsze myślałem, że z uwagi na to iż PHP jest językiem skryptowym wszysto co jest związane z procesem budowania i kompilacji – nie dotyczy PHP właśnie…

Tzn. zawsze wiedziałem, że kompilacja się odbywa podczas wykonywania kodu, więc mnie nie zajmowała. Klawisz F9 z Delphi czy BorlandBuildera zastąpiony został przez klawisz F5 w przeglądarce ;)

Jeśli chodzi o budowanie (Crtl+F9 zdaje się ;)) to wiedziałem, że zawarta jest w tym procesie. Reszta mnie nie interesowała, więc osunąłem się w słodkie objęcia ignorancji.

Czytając książki sugerujące stosowanie systemów ciągłej integracji (CI) i konsolidacji myślałem sobie, że spoko, fajnie by było, ale jednak w PHP nie ma czegoś takiego jak integracja i konsolidacja.

Nie mogłem bardziej się mylić ;) Dopiero przy trzeciej książce, w której wspomniana jest CI mnie olśniło:
Budowanie aplikacji to cały proces, który zmienia czysty kod z repozytorium w działającą aplikację!

A zatem obejmuje stworzenie bazy danych o odpowiedniej strukturze(*), ustawienie odpowiednich uprawnień do katalogów, które tego wymagają, wypełnienie bazy danymi startowymi i wszystkie inne czynności wymagane do poprawnego używania aplikacji.

Niezwykle ważne jest, aby móc cały ten proces zautomatyzować. Jedna z rzeczy jakich nauczyłem się podczas mojego, dwuletniego już, stażu w zawodzie to:
Jeśli jest do wykonania skończony ciąg operacji oraz
informacja o tych operacjach zapisana jest w pamięci ludzkiej
to
istnieje skończona, stosunkowo mała, liczba powtórzeń ciągu tych operacji, że
przynajmniej jedna z operacji nie zostanie wykonana.

Nazywę tą regułę “Zasadą zapominania”. Dlatego warto zautomatyzować ten proces przy pomocy chociażby narzędzia Phing (jest to php’owy odpowiednik Ant’a).

Możliwe, że niedługo napisze Wam co i jak z tym Phing.

* Nigdy nie wiedziałem dlaczego przechowywanie struktury bazy w repozytorium jest moją obsesją. Widać podświadomie czułem, że kiedyś będę używał Project Build System(s).

Share Button

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:


/**
* 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:

class Medium extends AppModel {

var $name = ‘Medium’;
var $actsAs = array(‘Image’);

}

Kontroler:

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:

create(
array(‘url’ => array(
‘controller’ => ‘media’,

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

file(‘Medium.file’); ?>
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:


image($medium['Medium']['file']); ?>

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


Aktualizacja: po umieszczeniu tego pomysłu na bakery niejaki Travis Rowland wrzucił ciekawe poprawki. Nie miałem okazji ich wypróbować, ale dla zainteresowanych: http://bakery.cakephp.org/articles/view/imagebehavior-best-from-database-blobs-and-file-storage

Share Button

Czego brakuje frameworkowi cakePHP?

Jakiś czas temu zetknąłem się z frameworkiem Django dla języka Python. Oprócz samego języka, który jest zdecydowanie bardziej nowoczesny w Django szczególnie urzekła mnie jedna rzecz.
Chodzi o sposób definiowania struktury dla bazy danych. Otóż w Django odbywa się to tylko w jednym miejscu – w plikach modeli.

Dlaczego jest to takie fajne? Posłużę się antyprzykładem z cakePHP.
Wyobraź sobie, że masz projekt i zarządzasz jego wersjami za pomocą popularnego SVN, czy CVS. Najczęściej pracujesz na lokalu i we wczesnych fazach projektu to dodasz pole w bazie danych w jakimś miejscu, do zmienisz indeks. Robisz commit kodu, który na nowej strukturze działa dobrze, ale struktura bazy danych nie podlega wersjonowaniu.
Zatem albo musisz zrobić eksport struktury do pliku który jest wersjonowany, albo (jak często odbywa się to w moich projektach) – robisz zmiany na pewnej głównej i ogólno dostępnej bazie (co by reszta mogła sobie ściągnąć nową wersję).

Pierwsze rozwiązanie sprawia, że przybywa Ci pracy. Wiem, że to nie dużo, ale zawsze jedna dodatkowa rzecz o której musisz pamiętać. A w przypadku gdy commit zrobiłeś po tygodniu pracy, bo wcześniej cały system się sypał – możesz zapomnieć o takich szczegółach jak dodanie pola na początku.
W przypadku jest jeszcze gorzej. Oprócz problemów opisanych powyżej dochodzi niebezpieczeństwo usunięcia całej bazy, brak możliwości powrotu z bazą do wcześniejszej wersji.

Z kolei w django możesz zapomnieć o phpMyAdminie i innych. Całą strukturę bazy danych masz zdefiniowaną w modelu i jednym poleceniem synchronizujesz jej wersję w silniku bazy z definicją w modelu.

Oczywiście do podejścia trzeba się przyzwyczaić.  W cake’u trzymając się konwencji wszystkie modele mogą wyglądać tak:
class TabelaWFormieLiczbyPojedynczej extends AppModel {};
W django – to co nie istnieje w modelu – nie istnieje w bazie.

Ciekaw tylko jestem jak to się sprawdza przy nietypowych sytuacjach, które nieraz wymagają nieco karkołomnych konstrukcji w projekcie bazy danych i specyficznych zapytań sql. Moja znajomość samego Django jest na ten moment za mała, żeby odpowiedzieć na to pytanie.

Share Button