//DEVGURU

Category archives ‘CakePHP’

UploadPack – łatwy i elastyczny system uploadu plików w CakePHP

Tuesday, November 4th, 2008

Mimo, że upload plików to funkcjonalność obecna w wielu aplikacjach internetowych, do tej pory trudno znaleźć rozwiązanie, które by w znaczący sposób ułatwiało obsługę tego procesu z poziomu aplikacji w CakePHP.

Widziałem i używałem już paru skryptów czy pluginów obsługujących upload po stronie serwera (napisanych nie tylko w PHP), dzięki temu mogłem stworzyć sobie wizję takiego pluginu, dostosować ją do specyfiki frameworka CakePHP i spróbować wprowadzić ją w życie.

Postawione wymagania:

  • zapis rekordu z załącznikiem nie powinien się niczym różnić od zapisu zwykłego rekordu
  • całość powinna działać przy minimalnej konfiguracji, ale również być łatwo dostosowywalna do potrzeb aplikacji
  • możliwość wykonywania dodatkowych czynności z przesłanym plikiem niż tylko zapis na dysk, np. wygenerowanie miniaturek obrazka
  • łatwy dostęp do plików, także do ich różnych wersji (miniaturek) z poziomu widoku
  • naturalna integracja z CakePHP

Efektem pracy jest UploadPack, który w chwili obecnej zawiera:

  • behavior, który podłączony do modelu zajmuje się zapisem plików na dysk i ewentualnie generowaniem miniaturek obrazków
  • helper, który ułatwia wyświetlanie url’i do przesłanych plików i wyświetlanie obrazków

Wydaje mi się, że całość działa dość zgrabnie. Jedyną rzeczą, którą trzeba zrobić jest dodanie pola w bazie danych przechowującego nazwę pliku i podpięcie behaviora do modelu. Reszta idzie automatycznie. Wszystko jest udokumentowane na stronie repozytorium UploadPack.

Na razie dostępna jest wersja 0.1, z którą jednak można już całkiem sporo zdziałać. Prace nad dalszymi funkcjonalnościami trwają :)

Named scope w CakePHP

Sunday, August 31st, 2008

Kilka dni temu napisałem jak ulepszyć system callbacków w kontrolerach CakePHP wzorując się na rozwiązaniu zastosowanym w Ruby on Rails. Dziś przeniesiemy kolejny fajny feature z Railsów do Cake’a – named scope.

Named scope czyli po polsku “nazwany zakres/dziedzina”. Chodzi o definicję podzbiorów rekordów danego modelu w celu późniejszego łatwiejszego ich wyszukiwania.

Zaczniemy znowu od przykładu. Definicja modelu User w RoR:

class User < ActiveRecord::Base
  # podzbiór użytkowników, którzy aktywowali konto
  named_scope :activated, :conditions => "activated_at is not null" 

  # podzbiór użytkoników, którzy są on-line
  named_scope :online, :conditions => "date_add(last_activity, interval 5 minute) > now()"
end

To bardzo prosty przykład, w rzeczywistości named scope nadaje się też do dużo bardziej ciekawych rzeczy. Jednak “wersja Cake’owa”, którą zaprezentuje poniżej jest właśnie ograniczona do definicji warunków.

Zdefiniowane wyżej zakresy możemy później użyć podczas wyszukiwania:

# wszyscy aktywowani użytkownicy
users = User.activated.find(:all) 

# wszyscy aktywowani i on-line użytkownicy z liczbą punktów większą od 10
users = User.activated.online.find(:all, :conditions => "points > 10")

Zalety zapisu widać właściwie od razu. Named scope upraszcza zapis. Często powtarzające się warunki wyszukiwania (dostęp tylko do aktywowanych użytkowników potrzebujemy w wielu miejscach aplikacji) umieszczamy w named scope, a inne umieszczamy “po staremu” w :conditions. Zapis wygodny i bardzo czytelny.

Podobną funkcjonalność w Cake’u zapewni nam NamedScopeBehavior, który przetłumaczy (w callbacku modelu beforeFind) zdefiniowane zakresy na warunki przekazane później do zapytania SQL.

Plik named_scope.php pobrany repozytorium należy umieścić w folderze app/models/behaviors. Później w modelu możemy już definiować zakresy.

class User extends AppModel {
  var $actsAs = array(
    'NamedScope' => array(
      'activated' => array('User.activated in not null'),
      'online' => array('date_add(User.last_activity, interval 5 minute) > now()')
    )
  );
}

Teraz już możemy zacząć korzystać.

$this->User->find('all', array('scope' => 'activated'));
$this->User->find('all',
  array('conditions' => 'points > 10', 'scope' => array('activated', 'online')));

Paginacja działa równie dobrze:

$paginate = array(
  'User' => array(
    'order' => 'created ASC',
    'limit' => 20,
    'scope' => array('online', 'activated')
  )
);

Mam nadzieję, że NamedScopeBehavior ułatwi i uprzyjemni Wam trochę development w CakePHP :)

Ulepszony system callback’ów w kontrolerach CakePHP

Friday, August 22nd, 2008

Callbacki w kontrolerach (beforeFilter, beforeRender lub afterFilter) to bardzo użyteczne narzędzia, pozwalają zaoszczędzić sporo czasu, czynią kod bardziej czytelnym i bardziej DRY. Jednym słowem miód… chyba, że zrobią się zbyt duże.

Sprawa najczęściej wygląda tak:

  • chcemy, żeby jakiś kod wykonał się przed każdym requestem w kontrolerze, więc wrzucamy go do beforeFilter
  • z kolei inne rzeczy mają się wykonywać tylko przed niektórymi akcjami, więc je też wrzucamy do beforeFilter dodając jeszcze blok if-then-else, który zdecyduje kiedy je wykonać
  • mamy jeszcze trochę kodu, który chcemy wykonać przed requestem, ale tylko gdy użytkownik jest zalogowany, kolejny if-then-else ląduje w beforeFilter
  • i tak dalej, aż przestajemy nad wszystkim panować

Można by zrobić to lepiej? No cóż, spójrzmy na poniższy kawałek kodu z Ruby on Rails.

class UsersController < ApplicationController
  before_filter :do_something, :do_always
  before_filter :do_something_else, :only => [:show, :new]
  before_filter :do_something_different, :except => :edit

  # method definitions commented out
end

Oto co tu się dzieje:

  • metody do_something i do_always zostaną wykonane przed każdym requestem
  • do_something_else tylko przed akcjami show i new
  • do_something_different przed wszystkimi akcjami oprócz edit

Prosto i czysto. Każdy kawałek kodu, który chcemy wykonać w before_filter (niezależnie od warunków) ląduje w osobnej metodzie kontrolera. Kontroler sam decyduje co i kiedy wykonać.

Jest to łatwiejsze w zarządzaniu i testowaniu niż podejście zastosowane w CakePHP. Więc… przenieśmy je do Cake’a.

Kod AppControllera do pobrania z pastie.

Teraz możemy już robić takie rzeczy:

  var $beforeFilter = array(
    array(
      'methods' => array('do_something', 'do_always')
    ),
    array(
      'methods' => array('do_something_else'),
      'only' => array('show', 'new')
    ),
    array(
      'methods' => array('do_something_different'),
      'except' => array('edit')
    )
  );

  function _do_something() { }
  function _do_always() { }
  function _do_something_else() { }
  function _do_something_different { }

Powyższy kod jest ekwiwalentem kodu kontrolera w Ruby on Rails z początku postu. Znaki podkreślenia przed nazwami metod pozwalają je odróżnić od regularnych akcji zdefiniowanych w kontrolerze.

Dodatkowo możemy także określić inne warunki wykonania metod przez callback.

  var $beforeFilter = array(
    array(
      'methods' => array('do_something'),
      'if' => array('is_admin')
    ),
    array(
      'methods' => array('do_something_else'),
      'unless' => array('is_logged_in')
    )
  );

  function _logged_in() { }
  function _is_admin() { }

Teraz metoda do_something zostanie wywołana tylko gdy _is_admin zwróci true, a do_something_else gdy _is_logged_in zwróci false. Zresztą kod mówi sam za siebie.

Oczywiście wszystkie opcje (only, except, if, unless) mogą być używane razem lub w dowolnych konfiuracjach.

Definicje w tabeli $beforeFilter (lub $beforeRender, lub $afterFilter) będą analizowane w kolejności w jakiej się się w niej pojawią (o ile oczywiście ich warunki zostaną spełnione).

Dodatkowo dzięki rozszerzeniu metody __mergeVars, która jest automatycznie wywoływana przy inicjalizacji kontrolera, możliwe jest definiowanie tablic również w AppControllerze i PluginAppControllerze. Wtedy pierwszeństwo mają te z AppControllera, następnie PluginAppControllera i na końcu ze zwykłego kontrolera.

Enjoy.

Zarządzanie asocjacjami typu Habtm w CakePHP

Sunday, June 1st, 2008

Asocjacje typu has and belongs to many w Cake PHP potrafią czasem nieźle napsuć krwi. Dodawanie i usuwanie powiązań rekordów jest dość pracochłonne, a co gorsze może być źródłem błędów, jeśli robić to za każdym razem ręcznie. Dziś wpadłem na rozwiązanie, które znacząco ułatwia życie w takich sytuacjach. Chodzi o zamieszczony już jakiś czas temu w bakery behavior ExtendAssociations.

Dzięki zastosowaniu tego rozwiązania możliwe są konstrukcje:

$this->Post->habtmAdd('Tag', $postId, $tagId);
...
$this->Post->habtmDelete('Tag', $postId, $tagId);
...
$this->Post->habtmDeleteAll('Tag', $postId);

Więcej szczegółów w podlinkowanym wyżej artykule. Polecam również przeczytanie zamieszczonych tam komentarzy, które sugerują możliwości dodatkowego usprawnienia tego behaviora.

Całość oczywiście nie jest tak fajna i intuicyjna jak np. w Railsach, ale taka już specyfika samego PHP. Mimo to polecam :)

CakePHP – dobre praktyki

Monday, February 25th, 2008

Pisząc małe aplikacje nie musimy się przejmować zbytnio jakością kodu, ale już nawet średnie projekty wymagają tego, by je w jakiś sposób zaplanować, żeby je w ogóle skończyć. Po napisaniu kilku rzeczy w Cake’u mam parę spostrzeżeń na to, jak pisać, by było dobrze (a przynajmniej by nie było źle). Część z poniższych punktów odnosi się do programowania w ogóle, część jest stricte Cake’owa.

1. Dobre zaplanowanie struktury aplikacji.

Rzecz najważniejsza. Przeanalizować założenia, zidentyfikować wszystkie obiekty, które będą występować w systemie. Dobrze rozbić aplikację na wiele rozłącznych modułów, które będą mogły być oddzielnie implementowane i testowane, a dopiero później ze sobą łączone. Korzyści są oczywiste – mniejsze powiązanie różnych części aplikacji ze sobą (łatwiejsza implementacja, testowanie), łatwość podziału prac pomiędzy wielu programistów.

2. Podejście abstrakcyjne do implementacji funkcjonalności

Poszczególne moduły aplikacji mogą zostać tak wykonane, żeby mogły być użyte w wielu kontekstach bez dokonywania w nich żadnych, bądź tylko kosmetyczno/konfiguracyjnych zmian. Dla przykładu funkcjonalność oceniania obiektów przez użytkownika można wykonać następująco:

tabela w bazie danych ratings:

  • id – primary key
  • user_id – id oceniającego użytkownika
  • model – nazwa modelu obiektu ocenianego
  • foreign_key – id obiektu ocenianego
  • rating – ocena

Taka struktura pozwala na ocenę każdego obiektu w serwisie. Do tego można dopisać sobie odpowiedni Behavior udostępniający w wybranych modelach metodę realizującą zapis oceny do bazy i mamy prosty ogólny system oceniania, do którego aktywacji wystarczy dodanie behaviora do $actsAs modelu.

3. Uniezależnienie kontrolerów od interfejsu użytkownika

Akcje w kontrolerach powinny być tak zaimplementowane, by nie było różnicy czy są wywoływane poprzez AJAX czy normalnie. To widoki (a właściwie w tym przypadku layouty) decydują jak zostaną zwrócone dane wynikowe przez akcję.

4. Przeniesienie większości logiki do modeli

To w modelach, nie w kontrolerach powinna być zaimplementowa większość logiki biznesowej systemu – zgodnie z koncepcją “Fat model, skinny controller”. Często wiele kontrolerów korzysta z tych samych modeli wykonując na nich te same funkcje. Przeniesienie tych funkcji do modelu pozwala łatwiej później wprowadzać zmiany i poprawiać ewentualne błędy, ponieważ musimy tego dokonywać tylko w jednym miejscu – w modelu. Podobnie implementacja takich mechanizmów jak caching czy logowanie zdarzeń jest dużo łatwiejsza do wykonania i utrzymania.

5. Elementy

Podobnie jest z elementami. W wielu widokach występują te same elementy, więc powinny być tworzone właśnie w takiej formie. Dla przykładu: formularze dodawania i edycji obiektu z reguły różnią się od siebie co najwyżej wartością atrybutu action, zatem idealnie nadają się, by zrobić z nich element.

To pierwsze modele i kontrolery aplikacji determinują jej strukturę i to, w jaki sposób jest dalej rozwijana. W połowie prac, bardzo trudno zmienić założenia co do struktury kodu, zatem analiza i planowanie powinny odbyć się jeszcze przed rozpoczęciem kodowania. I myślę, że nie należy na nie przesadnie szczędzić czasu, gdyż zwróci się on później na pewno.

CakePHP – Odc. I – Wariacje na temat ‘_queryCache’…

Saturday, October 13th, 2007

…czyli jak zadać dwa razy to samo pytanie i uzyskać inną odpowiedź.

akt 1. Cache’owanie zapytań

Cake zapamiętuje wynik każdego zapytania o czym nie można się dowiedzieć z manuala. Zapamiętuje jednak tylko w pamięci podręcznej, a więc jedyna korzyść z tego taka, że jak w tej samej metodzie (akcji) controllera wywołam np.

$user = $this->User->findAllByName('Alojzy');
...
$user = $this->User->findAllByName('Alojzy');

to zapytanie do bazy danych zostanie wykonane tylko raz. Dzieje się tak nawet, jeśli zamiast findAllByName skorzystamy z
$this->User->query(…)

Problem jednak pojawia się, gdy między tymi wywołaniami znajduje się coś jak:

$this->User->save($this->data);


wtedy drugie findAllByName zwróci nieaktualne dane (oczywiście o ile $this->data będzie zawierało informacje o jakimś Alojzym).

Sytuacji takiej oczywiście zwykle można uniknąć, bo jak odczytam dane z bazy, a potem je zmienię, to znaczy, że posiadam komplet wiedzy, żeby zbudować wynik kolejnego odczytu bez odwoływania się do bazy.

akt 2. Jak być powinno

Zgodnie z ideą CakePHP programista powinien dążyć, do traktowania modelu jako raczej reprezentanta konkretnego rekordu, a nie tabeli (oczywiście findAll i temu podobne to wyjątki). Dlatego przy zapisie danych ustalany jest klucz rekordu i wywołując ’saveField’, czy ’save’ bez klucza, odwołujemy się ciągle do tego samego rekordu. Ponieważ bywa to uciążliwe, gdy zapisuje się więcej niż jeden rekord (np. w pętli), Cake’owcy proponują w takim przypadku wywoływać:

$this->[nazwa_modelu]->create();


za każdym razem gdy skończymy odwoływać się do konkretnego rekordu (np. w tej pętli).

Wydaje sie naturalne, że wywołanie takiego resetu na modelu powinno wyczyścić jego cache (czy tylko mi się to wydaje naturalne?)

Niestety cache nie jest trzymany w klasie AppModel (model_php5.php), tylko w klasie DataSource (datasource.php i dbo_source.php). Dzięki temu zapytanie jest cache’owane jako jedno i to samo nawet gdy wywoływane jest poprzez różne modele (IMHO nieco wątpliwa korzyść). Utrudnia to jednak operowanie na cache’u.

akt 3. Rozwiązanie.

W przypadku, gdy chcę odczytać dane z bazy (select) przed i po modyfikacji danych (update), muszę po updacie wyczyścić cały cache zapytań. Na stronie: http://www.benjiegillam.com/serendipity/categories/11-CakePHP Benjie Gillam proponuje dodać do app/app_model.php następującą funkcję:

 function clearAllDBCache() {
   $db =& ConnectionManager::getDataSource($this->useDbConfig);
   $db->_queryCache = array();
  }

i wywoływać ją za każdym razem, gdy w środku controllera zapragniemy aktualnych danych.

Krzysiek Heród