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:

echo $form->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

Nam obiektowo pisać (nie) kazano…

… czyli co się dzieje, kiedy ktoś kto nie wie dlaczego się używa kodu obiektowego, pisze takowy bo musi.
Jest sobie jedna, bardzo wazna metoda w bardzo ważnej klasie, w bardzo ważnym projekcie. Ciągnie się od linii 484 do majaczącej na horyzoncie (i zdecydowanie poza ekranem) linii 745.
Jest w niej o wiele za dużo. Sprawdza czy dane są poprwane, zlicza, podlicza, zapisuje do bazy – sprawdza czy dobrze zapisało, w razie czego robi rollback. Do tego na koniec wysyła maila.
No i bach! Okazało się, że w mailu trzeba wysłać jedną dodatkową informację i jak jej teraz szukać. Tak bardzo chciałbym mieć tą funkcję zrefaktorowaną. Nie mieć zagnieżdżeń, tylko ciąg wywołań prywatnych metod:

$this->zrobTo();
$this->zrobTamto();
$foo = $this->znajdzToITamto($bar);
...
$this->wyslijMaila($dane, $temat, $inneDane);

I tylko w trzech kropkach dodać $dane = array_merge($dane, $foo), albo coś równie banalnego. Ale nie. Najpierw 5 minut na znalezienie gdzie ten cholerny mail jest wysyłany. 10 na zlokalizowanie tablicy z której dane trzeba dołączyć do maila. Potem z 20 minut na poprawki + sprawdzanie czy wsztstko jest tak jak powinno.
No i to nieodparte poczucie. Ten szept na ramieniu mówiący “Możesz być pewien, że w kodzie od teraz masz jedną dziurę więcej (buahahahaha)”.

Ale patrzę na motto tego bloga i myślę sobie – trzeba sobie radzić ze swoim kodem…

Jak korzystać z requestAction w CakePHP

Ta bardzo przydatna funkcja nie jest zbyt dobrze opisana w dokumentacji, dlatego pozwolę sobie ją tu opisać.

Object::requestAction(string $url, array $extra);
Służy do łatwego wywoływania funkcji z jednego kontrolera w innym kontrolerze. Sprawa jest prosta, jeżeli wywoływana funkcja potrzebuje jedynie parametrów, które możemy przekazać w url-u (/posts/show/1).
Jednak sprawa komplikuje się, kiedy potrzebna metoda korzysta z danych przesłanych w formularzu. Komplikuje się o tyle, że w wyniku ubogiej dokumentacji, trudno odgadnąć czy jest to możliwe.
Jednak m.in. po to jest parametr $extra. Jeśli wasz kontroler czeka na dane z formularza ['User']['username'] i ['User']['password'], to można je “wcisnąć” do kontrolera poprzez requestAction w następujący sposób:


$data['data'][['User'] = array('username'=> 'ala', 'password' => 'makota');
$this->requestAction('/users/login', $data);

Dzięki tej sztuczce uda Ci się ominąć kilka sytuacji, w których wcześniej pisalibyście osobną funkcję.

Ps. Cake jest pełen takich nieudokumentowanych niespodzianek, które czekają na odkrycie – zachęcam do eksperymentów.
Pps. Szczególnie przydatność requestAction odczujesz, kiedy zaczniesz intensywniej korzystać z pluginów w CakePhp.