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...

Pokazywanie postów oznaczonych etykietą CakePHP. Pokaż wszystkie posty
Pokazywanie postów oznaczonych etykietą CakePHP. Pokaż wszystkie posty

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

poniedziałek, 6 października 2008

Co się dzieje, gdy dane są nie tylko w bazie?

Z tym problemem często spotykam się w pracy.

Standardowe zagadnienie - klient chce wrzucać obrazki na stronę, a my ze względu na bazę zapisujemy je jako pliki, a w bazie co najwyżej ścieżkę do niego.
No i niestety przeniesienie systemu na inny serwer (np. produkcyjny) to, oprócz kopiowania kodu i bazy, przenoszenie multimediów, które nie zawsze jest miłe i przyjemne.

Dlatego od jakiegoś czasu chodzi mi po głowie pewna koncepcja, która ten problem mogłaby rozwiązać.
Otóż pliki binarne, podczas uploadu należało by jednak zapisać w bazie. Do tego należałoby opracować komponent, który w przypadku żądania obrazka o id = 1 sprawdziłby odpowiedni katalog i po znalezieniu go - zwrócił jako odpowiedź. Z kolei przy jego braku w systemie plików - utworzył takowy na podstawie danych w bazie i w standardowy sposób zwrócił plik jako odpowiedź.

Świetnie by się do tego nadawały cake'owe behaviors. Do tego można by go sprząc z jeszcze jedną funkcjonalnością- plik mógłby być w bazie zapisywany (w razie podmiany zawartości) za każdym razem pod inną nazwą (np. id_kolejny_numer_wersji). Dzięki temu możnaby tym plikom ustawić nagłówki Expire z odległą przyszłością i korzystać z dobrodziejstw bufora przeglądarek...

środa, 1 października 2008

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.

poniedziałek, 4 sierpnia 2008

HttpSocket z cake 1.2 w cake 1.1.x

Jeśli potrzebujesz funkcjonalności zapewnianej przez HttpSocket(na przykład musisz pracować na danych w xml-u dostarczanych choćby przez kanał rss), a z jakichś powodów nie możesz korzystać/migrować na CakePhp1.2, zastosuj poniższą sztuczkę:

Z biblioteki CakePhp1.2 (cake/libs/) do katalogu vendors w Twojej aplikacji (1.1.x) skopiuj pliki
- socket.php
- http_socket.php

W pliku socket.php gdzieś na początku, przed definicją klasy jest linia
uses('validation');
zakomentuj ją (lub usuń) .
Z kolei w pliku http_socket.php linię
uses( 'socket', 'set');
Zamień na
vendor('socket');
uses('set');

Sposób działający pomiedzy wersją 1.2.0.6311 beta a 1.1.16.5421.
Powodzenia

czwartek, 19 czerwca 2008

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.

poniedziałek, 19 maja 2008

Bardziej przyjazne linki w CakePHP

Załóżmy, że mamy naszego nieśmiertelnego bloga.
Aby wyświetlić dany post, generujemy mniej więcej taki link za pomocą helpera html:
http://domena.pl/posts/show/23

Powiedzmy, że chcemy być SEO friendly i doklejamy na końcu tytuł postu:
http://domena.pl/posts/show/23/bardziej_przyjazne_linki_w_cakephp

Niby wszystko ładnie, pięknie, ale słowa posts i show w adresie zmniejszają siłę pozostałych słów, i strona będzie się pozycjonować w google gorzej np. na słowa linki + cakephp.

Lepszym byłby link:
http://domena.pl/23/bardziej_przyjazne_linki_w_cakephp

Można łatwo sprawić, żeby tak to działało, bez większych problemów.

Przede wszystkim edytujemy app/config/routes.php
i dodajemy do nich linię:

$Route->connect('/:id/*', array('controller' => 'posts', 'action' => 'show'));

dzięki temu zabiegowi ruter będzie nam linkował adresy w takiej postaci do kontrolera posts, metody show.

Teraz należy zmodyfikować metodę show, aby działała przy takim przekierowaniu:
1. umożliw wywołanie funkcji bez podania parametru id:

function show($id=null){

2. w wypadku nowego przekierowania id znajduje się w $this->params['id'] :

$id = (isset($this->params['id']))? $this->params['id']: $id;

Bardzo fajnie, ale teraz przestaje działać stary (standardowy) sposób przekierowania taki adres:
http://domena.pl/posts/show/23
zwróci błąd missing controller.

Łatwo temu zaradzić dodając jeszcze jedną linię w pliku routes.php:

$Route->connect('/static_pages/view/:id/*', array('controller' => 'static_pages', 'action' => 'view'));

Koniecznie powyżej tej poprzednio dodanej!

poniedziałek, 5 maja 2008

Odpowiednie narzędzia

W poprzednich postach( Czego tak na prawdę ode mnie chcesz [kliencie] ? i Funkcjonalność czy bajery?) napisałem o podstawowych niebezpieczeństwach jakie czyhają na nas w procesie tworzenia dedykowanego systemu dla klienta. W obu przypadkach w pewnym sensie ochroną przed tymi zagrożeniami będzie odpowiedni framework. Poznałem (lepiej lub gorzej) do tej pory trzy frameworki: CakePHP, ZendFramework i Django. Intensywnie pracuję z CakePHP i na nim oprę ten artykuł, pozostałe znam na tyle, żeby mieć pewność, że w podobny sposób pomagają w produkcji oprogramowania i żeby nie musieć opisywać każdego z osobna.

Głównym celem frameworku jest usprawnienie pracy programisty. Ma on dostarczać mechanizmy, które odciążają nas od żmudnego powtarzania nieciekawych czynności (łączenie z bazą, tworzenie milionowego formularza, pisanie kolejnego foreach żeby wydłubać coś z tablicy...). Nieciekawych oczywiście z punktu widzenia doświadczonego programisty, który robi to już długo - zaczynając przygodę z tą dziedziną wszystko jest ciekawe, ale ten stan po jakimś czasie mija.

Jeśli jesteś kompletnie zielony - odradzam zaczynanie przygody z programowaniem od poznania frameworku. Wydaje mi się, że postępując w ten sposób trudniej będzie Ci zrozumieć mechanizmy w nim zachodzące i zamiast mieć możliwość pełnego wykorzystania narzędzia, stanie się on dla Ciebie magiczną różdżką, której potencjału nie wykorzystasz. (oczywiście są wyjątki i możesz robić co chcesz).

Ja jednak nie będę skupiał się na dostarczanych przez framework bibliotekach, bo o tym już wiele napisano. Spróbuję za to napisać jak można go wykorzystać do przeciwdziałania trudnym tendencjom pojawiającym się w procesie produkcji i nie mających wiele wspólnego z samym programowaniem.

Na początek problem specyfikacji wymagań. Maksymalne starania Twoje i klienta, żeby dokładnie określić wymagania i tak nie zabezpieczą Cię w stu procentach. Zawsze po drodze okazuje się, że przydało by się coś innego. Coś mogło by działać lepiej i że pojawił się nowy pomysł. Tak jak z budowaniem samochodu - wkładając wysiłek w obliczenia będziesz pewny, że wycieraczki obejmą maksymalnie przednią szybę, ale dopiero jak się nim przejedziesz to stwierdzisz, że drążek zmiany biegów jest za krótki, a słupek boczny ogranicza Twoje pole widzenia. Gdybyś produkował samochody doświadczenie mógłbyś wykorzystać dopiero w następnym modelu. Ty jednak jesteś farciarzem jakich mało i produkujesz ciągi zer i jedynek, które możesz w każdej chwili zmienić/wyciąć/dodać! Mechanizm zwany w CakePHP Scaffold jest tym, co owo wycinanie, zmienianie i wklejanie czyni bezbolesnym w początkowej fazie produkcji.

W CakePHP scaffolding objawia się w dwóch formach. W pierwszej jest w stanie wygenerować fizycznie modele, kontrolery i widoki na podstawie struktury bazy danych (i podpowiedzi użytkownika). Jest to wykonywane przy pomocy skryptu bake.php znajdującego się w cake/libs/scripts (w wersji 1.19). Efektem tego jest powstanie wszystkich standardowych metod i widoków do operacji takich jak list/view/edit/add/delete. Z uwagi na to, że są one generowane fizycznie - możesz je zacząć zmieniać dostosowując je do swoich potrzeb. Jednak minusem jest to, że w razie zmiany struktury bazy trzeba albo na nowo wygenerować widoki i metody, albo edytować je ręcznie.
Drugą emanacją jest scaffold właściwy. Włącza się go w prosty sposób dodając w kontrolerze linię

var $scaffold;

(Tutaj uwaga - można włączyć scaffold 'globalnie' dodając tą zmienną w AppController, ale wtedy nie należy dodawać już jej w kontrolerach).
Efektem tego będzie każdorazowe generowanie metody i widoku dla standardowych operacji na podstawie struktury bazy danych (tej zdefiniowanej w modelach). Dziać się będzie to tak długo, aż nie usuniemy deklaracji zmiennej, albo nie zdefiniujemy w kontrolerze którejś z metod standardowych. W pierwszym przypadku scaffolding wyłączymy całkowicie dla danego kontrolera, w drugim nie będzie on działał dla metody, którą już zdefiniowaliśmy. Minusem jest to, że nie możemy dostosować widoku do naszych widzimisię (widok nie istnieje fizycznie na dysku)*, ale za to przy każdej zmianie struktury bazy danych widok jest uaktualniany bez naszej ingerencji (dodanie pola varchar zaowocuje pojawieniem się w formularzu pola input, text - textarea, enum - listy wyboru).
To rozwiązanie jest genialne w najwcześniejszej fazie produkcji oprogramowania. Wyobraź sobie, że po zaprojektowaniu samochodu od razu możesz się nim przejechać (fakt będzie brzydki, bez maski i tapicerki) i zobaczyć jak się nim jeździ i czy nie przydałby się większy bagażnik. Tak właśnie jest w tym wypadku: projektujesz bazę danych i już możesz pokazać klientowi podstawowe funkcjonalności**. Możecie z klientem UŻYWAĆ tej aplikacji i stwierdzić, że czegoś brakuje.
Oczywiście nie jest tak super cały czas - w pewnym momencie trzeba po prostu napisać trochę kodu, czasem aplikacji nie wpisują się tak łatwo w szablon listuj/wyświetl/dodaj/edytuj/usuń.
Jednak w 8 przypadkach na 10 sprawdza się doskonale. Dorzuć do tego dobrze zaprojektowaną, znormalizowaną bazę i początkowa faza, która obfituje w zmiany jest tak bezbolesna, jak to tylko możliwe.

* te pliki istnieją fizycznie na dysku, są cache'owane, ale nie nadaje się to do edycji (zostanie nadpisane prędzej, czy później). Do tego wydaje mi się, że istnieje możliwość edycji szablonu, który definiuje jak mają być tworzone widoki, jednak nie zajmowałem się tym do tej pory, więc nie będę o tym pisał.
** pod warunkiem, że nauczysz go patrzeć na system pod tym kątem. Jak klient chce tylko ładny interfejs i nie współpracuje to scaffold nie spełni się w 100%.

sobota, 22 marca 2008

"Sprytny" redirect (cakePHP 1.2)

Załóżmy, że mamy mały system bloga (posty + komentarze).
Cake upiekł za nas widoki i metody. No i powiedzmy, że teraz w post/view/[id_posta] chcesz umieścić możliwość dodania komentarza. Oczywiście tworzysz formularz, którego action='/comments/add' i w CommentsController::add() wpisujesz na sztywno (hard-coding) $this->redirect('/posts/view/'.$comment['post_id']).

Wszystko pięknie i ładnie działa, ale powiedzmy teraz chciałbyś w widoku posts/list_all, w którym wyświetlasz skrócone wersje wszystkich postów dać możliwość szybkiego dodania komentarza. No i znów pakujesz do środka formularz z action='/comments/add' i fajnie działa, ale redirect jest do jakiegoś jednego konkretnego posta, a nie listy w której szanowny user kliknął 'submit'. Niby nic złego się nie dzieje, ale szanowny user odczuwa dyskomfort, jest wręcz zagubiony.

Ostatnio wymyśliłem i z powodzeniem zastosowałem mały trik, który ten problem rozwiązuje w sposób uniwersalny i (w miarę) elegancki.
Zazwyczaj, jeśli tworzę formularz do innego kontrolera, to najczęściej wtedy wiem, gdzie po przerobieniu tych danych powinien się odbyć redirect... najczęściej z powrotem. Zatem preparuję parametr action w następujący sposób:

action=<?php echo $html->url('/comments/add') ?>?r=/posts/list_all/"

A w AppController
    function beforeRender(){        if(isset($this->params['url']['r'])){             $this->redirect($this->params['url']['r']);          }     }

Prosta metoda, która zawsze sprawdzi mi czy przekazałem informację o przekierowaniu i w razie czego ustawi odpowiedni adres przekierowania. Nie muszę pamiętać o tym w każdej metodzie, w której jest prawdopodobne, że będę chciał zrobić redirect w różne miejsca. Po prostu implementuję to na początku projektu i cieszę się kolejnymi zaoszczędzonymi minutami :D

Smacznych jajec ;)

środa, 5 marca 2008

Preload obrazków w cakePHP

Dla tych, dla których DRY nie jest tylko angielskim słowem napisanym z niewiadomych przyczyn wielkimi literami.
Usprawnienie preloadu obrazków w cakePHP może przydać się, gdy chcesz wyświetlić typową galerię: miniaturki + duży obraz zmieniający się po kliknięciu w miniaturkę. Kiedy zaimplementujesz preload obrazków, to znaczy, że jesteś świadomym budowniczym stron internetowych. Wiesz, że możesz wykorzystać przeglądarkę do przyśpieszenia działania Twojej strony, a nie tylko jej biernego wyświetlania. Zatem do dzieła!

Moje podejście obiera się na tzw. elemencie. Element to (wg. manuala CakePHP) "częściowy layout". Czyli cegiełki, z których możesz stworzyć swoje strony.

app/views/elements/image_preload.thtml:
<?php $images_count = count($images); $i = 0;?>  <script type="text/javascript"> //<![CDATA[  var _images = new Array();  <?php $i=0; foreach($images as $image): ?>    _images[<?= $i ?>] = new Image();   _images[<?= $i++ ?>].src = '<?= 'http://'.$_SERVER['SERVER_NAME'].$this->webroot.'img/'.$subdir.$image['filename'] ?>';     <?php endforeach; ?>       //]]> </script>
O co chodzi? Wpisz w google "javascript image preload" to się dowiesz :)
Jak z tego skorzystać? Już śpieszę z wyjaśnieniami:
W pliku Twojego layoutu, na samym końcu (przed </body> ofcoz) dodaj:
 <?php if(isset($images_to_preload) && isset($images_to_preload_subdir)): ?>   <?= $this->renderElement('image_preload', array('images'=>$images_to_preload, 'subdir'=>$images_to_preload_subdir)); ?>  <?php endif; ?>
A w metodzie odpowiedniego kontrolera wygeneruj tablicę z nazwami plików, które chcesz załadować do cache przeglądarki i przekaż do layoutu:
   $this->set('images_to_preload', $content['Photo']);    $this->set('images_to_preload_subdir', 'photos/'); 
U mnie akurat model Photo ma pole filename, więc tak napisałem sobie element. Do tego dodałem możliwość przekazania z kontrolera podkatalogu w którym są obrazki w /app/webroot/img (teraz już nie wiem dlaczego, ale jak to pisałem, na pewno było to uzasadnione ;))

wtorek, 4 marca 2008

webservices cakePHP pogryzły się z serwerem SOAP

W poprzednich postach pokazałem sposób na stworzenie serwera soap we frameworku CakePHP, a ten po pewnym czasie przestał działać. Okazało się, że włączenie webserwices w /app/config/core.php spowodował problem. Otóż po tej operacji link_do_aplikacji/soap nie oznaczał już linku do SoapController::index(), ale do Controller::index().
Po prostu po właczeniu webservices oczekiwał czegoś w stylu:
link_do_aplikacji/soap/stuff, który wywołał by StuffController::index() i wyrenderował widok w /app/views/stuff/soap/index.thtml.
Niby nic, a może życie uprzykrzyć.

czwartek, 21 lutego 2008

CakePHP + nuSOAP serwer + autoryzacja othAuth

Autoryzacja serwera soap jest o tyle niewdzięczna, o ile nie została zaimplementowana w samym protokole. Do tego sam cake nie ułatwia nam sprawy. Jednak po wielu dniach zmagań udało mi się nagiąć tą materię i z chęcią się doświadczeniami podzielę (tak naprawdę po uporaniu się z tym problemem postanowiłem założyć ten blog).
Załóżmy, że serwer, który opisałem w poprzednim poście już stoi. Załóżmy też, że jest zrobione wszystko, co musiało być zrobione, aby autoryzacja othAuth działała w naszej aplikacji (jeśli nie- przed kontynuowaniem zapraszam tutaj )
Po pierwsze zmieńmy w /app/config/core.php definicję stałej CAKE_SESSION_COOKIE na 'PHPSESSID' bez tego nie uda nam się "odzyskać" sesji znając jej id.
Dalej... logowanie "normalnego" usera wygląda mniej więcej tak:
/app/controllers/user
function login()
{

if(isset($this->params['data']))
{

$auth_num = $this->othAuth->login($this->params['data']['User']);

return $auth_num;// potrzebne dla soap-owego logowania (1 jeśli udane)
}
}


Wracajmy teraz szybko do naszego oczka w głowie: /app/controllers/soap_controller.php.
W metodzie _defineTypes dodajmy definicję złożonych typów:

$this->server->wsdl->addComplexType (
'LoginData',
'complexType',
'struct',
'all',
'',
array(
'Login' => array('name' => 'Login',
'type' => 'xsd:string'), //
'Password' => array('name' => 'Password',
'type' => 'xsd:string'), //
)
);
/**
* odpowiedĹş na logowanie
*/
$this->server->wsdl->addComplexType (
'LoginResponse',
'complexType',
'struct',
'all',
'',
array(
'Result' => array('name' => 'Result',
'type' => 'xsd:int'), //
'SID' => array('name' => 'SID',
'type' => 'xsd:string'), //
)
);


A w metodzie _registerMethods() zarejestrujmy metodę SoapController.login:

$this->server->register('SoapController.login',
array(
'SoapParam' => 'tns:LoginData'),
array(
'return' => 'tns:LoginResponse') ,
$this->namespace,
$this->namespace . '#login',
'logowanie'
);


Teraz właściwe logowanie:

function login($login_data) {
$login_details['User'] = array('username' => $login_data['Login'],
'password' => $login_data['Password'],
'cookie' => "0");
$result = $this->requestAction('users/login', array('data' => $login_details));
return array(
'Result' => $result, 'SID' => session_id() );
}


Kilka słów wyjaśnienia: w pierwszej linii tworzymy trochę sztucznie tablicę $login_details, która jest taka jaką oczekuje metoda UsersController::login() (zgodnie z zasadą DRY nie będziemy tworzyć osobnej metody dla logowania przez soap). Wywołujemy akcję users/login przekazując do niej spreparowane dane i zwracamy zgodnie z definicją typu LoginResponse- tablicę z danymi.


To była ta łatwiejsza część. Teraz to nad czym spędziłem kilka intensywnych dni- jak odzyskać sesję??
Od razu sprostowanie: wygląda na to, że sesja "trzyma się" tak długo, jak długo klient się nie rozłączy (testowałem to za pomocą również nuSOAP + php i w okresie trwania skryptu - było ok). Jednak ja potrzebowałem możliwości podania przez klienta id sesji, którą otrzymał podczas logowania. Jak to zrobić?
Po kolei:
Dodajmy typ, który będzie służył do przekazania informacji na temat zalogowanego usera:
$this->server->wsdl->addComplexType (
'UserDetails',
'complexType',
'struct',
'all',
'',
array(
'Id' => array('name' => 'id',
'type' => 'xsd:int'), //
'Username' => array('name' => 'Username',
'type' => 'xsd:string'), //
'Email' => array('name' => 'Email',
'type' => 'xsd:string'), //
'GroupId' => array('name' => 'GroupId',
'type' => 'xsd:string'), //
'Error' => array('name' => 'Error',
'type' => 'xsd:string')
)
);


Zarejestrujmy metodę SoapController.check():

$this->server->register('SoapController.check',
array(
'data' => 'xsd:string'),
array(
'return' => 'tns:UserDetails') ,
$this->namespace,
$this->namespace . '#check',
'logowanie'
);


I zdefiniujmy ją:

function check($data="") {
$data = $this->requestAction('/users/soapCheck', array('data' => $data, 'SID'=>$data));
$this->log($data, LOG_DEBUG);

if(
$data){
$return = array ( 'Id' => $data['User']['id'],
'Username' => $data['User']['username'],
'Email' => $data['User']['email'],
'GroupId' => $data['Group']['id'],
'Error' => 'ewrifing ok');
}else{
$return = array ( 'Id' => -1,
'Username' => ''
'Email' => '',
'GroupId' => '',
'Error' => 'not logged in');
}

return
$return ;
}


Wyjaśnienia: oprócz danych jak przy logowaniu pojawiło się jeszcze pole w tablicy 'SID', to tędy przekażemy informację, że chcemy "wymusić" jakieś id sesji.
Teraz metoda UsersController::soapCheck():


function
soapCheck($data) {
return $this->othAuth->getData();
}


No i na koniec gdzieś musimy wymusić inne id sesji (no bo nie ma przeglądarki, ani kochanych cookies, które zrobią to za nas). W app/app_controller.php:

if(isset($this->params['SID'])){
session_id($this->params['SID']);
}


I ok... jaaasne. Nie działa, prawda? Dlaczego? A to dlatego, że w momencie kiedy klient SOAP łączy się z naszym ukochanym serwerem SOAP wygląda to tak:
request (soap/index) -> app_controller -> soap_contropper -> check -> request (/users/soapCheck) -> app_controller*->users_controller -> soapCheck ...

miejsce oznaczone gwiazdką oznacza moment, kiedy app_controller dostaje info o tym, że jest jakieś SID... tylko że wtedy to już jest za późno (jak to mówią ślązocy: "po ptokach" :P) $this->requestAction to moment, kiedy ciasteczka już nie są wysyłane i szanowna pani Sesja ma już głęboko w ... nosie fakt, że tam jakieś id jest przesyłane.
Nie lękaj się jednak, jest na to rada:
zmień w app_controller poprzednio dodany blok na:
if(isset($this->params['url']['SID'])){
session_id($this->params
['url']['SID']);
}


I teraz łącząc się z serwerem do linku /twoja_aplikacja/soap/ doklejaj parametr SID=. W takim wypadku już przy pierwszym request->app_controller id sesji trafia do "świadomości" Sesji i zazwyczaj rozpatruje je pozytywnie.

Nie jest to może najbardziej eleganckie rozwiązanie. Jednak najlepsze na jakie teraz mnie stać ;) Jeśli masz coś fajnieszego- pisz w komentarzach, z chęcią się podszkolę :)

wtorek, 19 lutego 2008

CakePHP + serwer SOAP (pierwsze kroki)

Mimo tego, że cake chwali się, że jest WebServices ready tyczy się to jedynie tak zwanego routingu (np. gdy użyjesz users/index - będzie "normalnie" czyli wyrenderuje widok /vews/users/index.thtml, a gdy "xml/user/index" - /views/users/xml/index.thtml) ale nie jest to prawdziwe WebSerwice. Nie wiem ile jest sposobów na zmianę CakePHP w prawdziwy serwer SOAP, ja znam jeden i Wam go pokażę.

1. Potrzebna nam biblioteka SOAP (jako prawdziwy programiści pozwalamy, żeby najtrudniejsze rzeczy pisali za nas inni). Ja wybrałem NuSOAP. Wrzuć pliki do /app/vendors/nusoap/*.
2. Stwórz kontroler do obsługi Soap. W moim przypadku soap_controller.php
/**
* Soap Serwer
* @package package
*/
vendor('nusoap/nusoap');
class
SoapController extends AppController {
var
$name = 'Soap';
var
$server;
var
$namespace;
var
$layout = 'blank';
var
$components = array('othAuth','Conf');
var
$othAuthRestrictions = null;

/**
* Inicjalizacja ustawień serwera
*
*/
function _init() {
$this->namespace = $this->Session->host;
$this->server = new soap_server();
$this->server->debug_flag = false;
$this->server->configureWSDL('MyWsdl', $this->namespace, 'http://'.$_SERVER['HTTP_HOST'] . $this->webroot . $this->params['controller'] ); // należy nadpisać endpoint, gdyż domyślnie ustawi się na /twoja_aplikacja/app/index.php
$this->server->wsdl->schemaTargetNamespace = $this->namespace;
$this->_defineTypes();
$this->_registerMethods();
}

/**
* Serwer endpoint handler
*
*/
function index(){
Configure::write('debug', 0);

$this->_init();

$HTTP_RAW_POST_DATA = isset($GLOBALS['HTTP_RAW_POST_DATA']) ? $GLOBALS['HTTP_RAW_POST_DATA'] : '';
$this->server->service($HTTP_RAW_POST_DATA);
exit();
}


/**
* define types required by this server
* przyda się później
*/
function _defineTypes() {

}

function
_registerMethods() {

$this->server->register(
'SoapController.hello', // method name
array('name' => 'xsd:string'), // input parameters
array('return' => 'xsd:string'),
$this->namespace,
$this->namespace . '#hello',
'document', // style
'encoded' // use
);

}

function
hello($name){
return array(
'return' => 'hello, '.$name);
}

}

?>

3. Do tego stwórz model, który nic nie robi :)
class Soap extends AppModel {
var $name = 'Soap';
var $useTable = false;
}
?>


Teraz objaśnienia:
SoapController::index() - to twój endpoint serwera SOAP. SoapController::_registerMethods() zajmuje się rejestrowaniem metod dostępnych przez serwer. W tym wypadku tylko jednej: SoapController::hello (notacja metod soap to Klasa.metoda).
Metoda hello przyjmuje string jako parametr i zrwaca 'Hello, '+ to co zostało jej przekazane.
Przykładowy klient (poza cake.php):
require('./nusoap/nusoap.php');

function
pr($var) {
echo
"<pre>";
var_dump($var);
echo
"&lt/pre>";
}

/* create client */
$endpoint = "http://localhost/meta/application_in_cakePHP/soap/index";

$mynamespace = "";
$client = new soapclient($endpoint);

$err = $client->getError();
if (
$err) {
// Display the error
echo '

Constructor error: '

. $err . '

'
;
// At this point, you know the call that follows will fail
}

$response = $client->call('SoapController.hello', array('name' =>"Greg"));
echo
"Call SoapController.hello";
if (
$client->fault) {
echo
'

Fault: '

;
print_r($response);
echo
'

'
;
} else {
// Check for errors
$err = $client->getError();
if (
$err) {
// Display the error
echo '

Error: '

. $err . '

'
;
} else {
echo(
'response:');
pr($response);
}
}

?>


I ładny response:
string(11) "hello, Greg"
Dodatkowo, gdy w przeglądarce wpiszesz ścieżkę do kontrolera Soap zobaczysz ładną dokumentację Twojego serwera. A dodając do ścieżki ?wsdl - dokument wsdl.

żródła:
http://www.scottnichol.com/nusoapprog.htm
http://dietrich.ganx4.com/nusoap/faq.php


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.