//DEVGURU

Author's entries:

Turning off auto timestamping for testing in Rails

Saturday, May 9th, 2009

Suppose that you implemented a functionality that depends on values of created_at or updated_at fields of your models. How do you test it?

If you use fixtures that reside in test/fixture/*.yml files then there is no problem, because the values you set there for created_at and updated_at fields are saved to the database ‘as is’. So you can easily have an article created one week ago:
[sourcecode lang="yml"]article:
title: What a great day
created_at: < %= 1.week.ago.to_s(:db) %>
updated_at: < %= 1.week.ago.to_s(:db) %>
[/sourcecode]

However, I don’t use fixtures files myself. I feel a bit dirty using them ;) I find fixture replacement tools far more maintainable. Namely, I love thoughtbot’s Factory Girl. But here comes the problem. This won’t work as expected with Factory Girl:


Factory(:article, :created_at => 1.week.ago, :updated_at => 1.week.ago)

That’s because ActiveRecord’s automatic timestamping feature sets Time.now for created_at and updated_at fields overriding our values. At least that’s ActiveRecord’s default behavior. Fortunately it can be disabled with:


Article.record_timestamps = false

Chances are that after creating a model with a custom timestamp we’ll want to turn automatic timestamping back on. But turning it off and on in many places in your unit tests would be pretty cumbersome. Wouldn’t it be cool if you could achieve all of this with a snippet below?

without_timestamping_of Article do
  Factory(:article, :created_at => 1.week.ago, :updated_at => 1.week.ago)
end

It turns timestamping off, executes the block and turns timestamping back on. I find it clean and dry. Here’s the code to place in your test_helper.rb:


# test_helper.rb
class Test::Unit::TestCase # or class ActiveSupport::TestCase in Rails 2.3.x
  def without_timestamping_of(*klasses)
    if block_given?
      klasses.delete_if { |klass| !klass.record_timestamps }
      klasses.each { |klass| klass.record_timestamps = false }
      begin
        yield
      ensure
        klasses.each { |klass| klass.record_timestamps = true }
      end
    end
  end
end

Hope you like it. If so, share :)

Dynamic cookie domains with Rack’s middleware

Friday, April 17th, 2009

Handling sessions in multi-domain environment is not the simplest things to do, because of the fact that cookies are scoped to a domain they were set by.

Recently we were developing an application with such an idea in mind:

  • Application will work as a base for other mini-applications (which we call sites)
  • Each site can be accessed via different url types: site.example.org and example.org/site
  • We want the users to remain logged in when switching from one url type to another

I won’t be covering application structure, routing, etc. here, I will only write about maintaing the sessions is such an environment.

So this is pretty simple here – all that we needed to do was to set cookie domain to .example.org (note the “dot” at the beginning). This could be done via:

ActionController::Base.session = {
  :domain => ".example.org"
}

However there was an additional requirement that we need to deal with:

  • Each site can be accessed via custom domain – site.com
  • Of course there’s no way here to keep the user logged in when he’s switching from site.com to example.org/site or site.example.org, at least it cannot be done with setting cookie domain to whatever value

Technically, to access the site via site.com, that domain must point to our IP address. Then we need to detect that the site is being accessed via custom domain and set cookie domain respectively.

This could be done via some funky before_filters in an Application Controller, however we found much better and cleaner way.

Rack’s middleware to the rescue

Rack itself is a minimal interface between web server and your ruby framework. It’s used by Ruby on Rails (since 2.3) and Merb. The request comes from web server, goes through middleware layers and enters the application.

So we wrote a middleware layer that detects the host with which our application is accessed and sets cookie domain for the request. Here it is:

app/middlewares/set_cookie_domain.rb

class SetCookieDomain
  def initialize(app, default_domain)
    @app = app
    @default_domain = default_domain
  end

  def call(env)
    host = env["HTTP_HOST"].split(':').first
    env["rack.session.options"][:domain] = custom_domain?(host) ? ".#{host}" : "#{@default_domain}"
    @app.call(env)
  end

  def custom_domain?(host)
    domain = @default_domain.sub(/^\./, '')
    host !~ Regexp.new("#{domain}$", Regexp::IGNORECASE)
  end
end

Now we need to turn it on:

environment.rb

config.load_paths += %W( #{RAILS_ROOT}/app/middlewares )

production.rb

config.middleware.use "SetCookieDomain", ".example.org"

.example.org is the default domain that will be used unless the application is accessed via custom domain (like site.com), we give it different values depending on environment (production/staging/development etc).

And since we’re fans of test driven development, here’s the test that ensures us that everything works as expected:

tests/integration/set_cookie_domain_test.rb

require 'test_helper'

class SetCookieDomainTest < ActionController::IntegrationTest

  context "when accessing site at example.org" do
    setup do
      host! 'example.org'
      visit '/'
    end

    should "set cookie_domain to .example.org" do
      assert_equal '.example.org', @integration_session.controller.request.session_options[:domain]
    end
  end

  context "when accessing site at site.com" do
    setup do
      host! 'site.com'
      visit '/'
    end

    should "set cookie_domain to .site.com" do
      assert_equal '.site.com', @integration_session.controller.request.session_options[:domain]
    end
  end

  context "when accessing site at site.example.org" do
    setup do
      host! 'site.example.org'
      visit '/'
    end

    should "set cookie_domain to .example.org" do
      assert_equal '.example.org', @integration_session.controller.request.session_options[:domain]
    end
  end

end

Test is sponsored by great Shoulda and Webrat gems.

Feel free to comment and share.

Websecurity through conventions and best practices

Friday, March 27th, 2009

Let me present you materials from a talk I gave at IT Underground Conference in Prague on 24.03.2009. The title of a presentation is “Websecurity through conventions and best practices” and it’s focused on securing web applications from the most common attacks with examples from Ruby on Rails web development framework.

The slides include all speaker’s notes.

Rails 2.2.2, Ajax and respond_to

Saturday, January 31st, 2009

As I wrote some time ago in the article about Rails, Ajax and jQuery, sometimes there are problems with Rails not interpreting correctly content type headers of ajax requests. It’s because not all web browsers send that header in the same way.

What I proposed was to sort the request.accepts array (array containing content type headers sent by browser) so that xml content type would be the first element. That would then trigger format.xml in our respond_to block.

However that approach does not work in Rails 2.2.2, because now the request.accepts array is frozen and it cannot be modified. I spent some time googling for the solution, but with no effect. So I dived into the API and Rails’ source code and came up with pretty nice and simple solution to the problem.

class ApplicationController < ActionController::Base
  before_filter :xhr_to_xml

  def xhr_to_xml
    request.format = :xml if request.xhr?
  end
end

This piece of code is an equivalent of the snippet I proposed in the article I referred to at the beginning. Now all ajax request will trigger format.xml in respond_to blocks.

What stuff I found in Rails 2.2

Monday, January 26th, 2009
When I started our first serious project in Rails the latest stable version was 2.1. Altrough I were still learning the framework, it quickly became obvious that some of it’s internals could have been implemented better. And in fact the next stable version, 2.2, which was released few months ago, was improved in many of the aspects I were thinking of.  Here are the new features I welcomed the most:

  • Added :only and :except to map.resources to let people cut down on the number of redundant routes in an application. Typically only useful for huge routesets.

You can find :only and :except in many places in the rails application and now, finally, in routes too. Usage:

  • Fixed that polymorphic_url should compact given array.

That will definitely dry up some code if you’re using polymorphic routes extensively.

  • Routes may be restricted to lists of HTTP methods instead of a single method or :any.

I could never really understand why I need to specify :any if I only want to allow POST and GET in my actions and not PUT or DELETE. Not more such problems after that fix. Usage:

  • Add :as option to render a collection of partials with a custom local variable name.

That is cool. No more setting :locals or variable renaming inside the parial. Usage:

  • Added block-call style to link_to

Nice alternative to one-line link_to’s. Helps to format HTML in links nicely. Usage:

  • Merge scoped :joins together instead of overwriting them.

This should be there much earlier. Now be careful, this may expose scoping bugs in your code when you’re migrating from earlier versions of Rails.

  • before_save, before_validation and before_destroy callbacks that return false will now ROLLBACK the transaction. Previously this would have been committed before the processing was aborted.

I never realized that integrity of my Rails 2.1 app database could be abused because of before_* callbacks breaking the action chain. Now I do and I am happy that I’m protected.

  • Allow conditions on multiple tables to be specified using hash.

That is something I was intuitively using when learning Rails which was resulting with errors as this feature had not been implemented at that time. Now it is, great.

  • Wrapped Rails.env in StringInquirer so you can do Rails.env.development?

Nice and pretty looking. No more messing with ENV array.

When I was preparing to making the switch to 2.2 I found out that Rails API documentation is missing lot of stuff. Fortunately that stuff can still be found in Rails changelogs and I recommend that you read them!

Rails, Ajax i jQuery

Tuesday, December 9th, 2008

Poniżej przedstawiam swoje podejście do wprowadzania ajaxowych funkcjonalności do aplikacji napisanych w Ruby on Rails. Jest to zbitka rozwiązań znalezionych na różnych blogach, forach oraz trochę moich przemyśleń. Do obsługi Javascript używam biblioteki jQuery, nie korzystam z jRails ani żadnych helperów do JS/Ajax dostępnych z Rails. Dzięki temu cały kod javascriptowy/htmlowy zaprezentowany poniżej może być bez trudu użyty w aplikacjach napisanych w innych językach/frameworkach. Zacznijmy.

Ruby on Rails a REST

Requesty ajaxowe różnią się trochę od zwykłych requestów, które dochodzą do serwera gdy użytkownik wpisze URL w przeglądarce. A przynajmniej powinny się różnić, żebyśmy po stronie serwera wiedzieli jaką odpowiedź (widok) zwrócić, nie ma bowiem sensu renderować całego layoutu np. dla requestu ajaxowego oceniającego zdjęcie w naszym serwisie. Tym aspektem odróżniającym różne typy requestów jest w tym przypadku nagłówek zawierający oczekiwany format odpowiedzi od serwera.

Dzięki implementacji REST w Railsach, sposób w jaki obsługujemy akcje jest zawsze taki sam niezależnie od typu requestu, jedyna zmiana dotyczy zwracanego przez nas widoku. Decydujemy o tym w kontrolerze:

To, który blok zostanie wywołany i w efekcie tego jaki widok zwrócony, zależy od route’ów zdefiniowanych w routes.rb oraz od oczekiwanego przez klienta formatu danych. W większości chcielibyśmy, żeby wszelkie ajaxowe requesty oczekiwały formatu XML (w odróżnieniu od HTML dla normalnych requestów), dlatego musimy zadbać o to, by przeglądarka odpowiednio ustawiała nagłówek Accept na ‘text/xml’ przy wywołaniach ajaxowych. W jQuery możemy ustawić to od razu dla wszystkich requestów z góry:

Różnice między przeglądarkami

Ręczne ustawienie nagłówka Accept niestety potrafi dawać różne efekty w różnych przeglądarkach. Wartością domyślnie ustawianą w przeglądarkach dla tego nagłówka jest text/html. Przy wywołaniu powyższego kodu Firefox zmieni wartość na ‘text/xml’, podczas gdy niektóre wersje IE i Safari ustawią ‘text/html; text/xml’ oczekując w ten sposób odpowiedzi od serwera w jednym z tych dwóch formatów.

Jest to dość istotna różnica, ponieważ Railsy decydując o tym, który format odpowiedzi wybrać, biorą pod uwagę pierwszy format napotkany w tablicy request.accept, którym w przypadku IE i Safari będzie text/html. Takie zachowanie zaobserwowałem chociażby w wersji 2.1.0 Railsów.

Poniższy kod naprawia opisane zachowanie przenosząc format xml na początek tablicy (jeśli się tam w ogóle znajduje), dzięki czemu będzie on znaleziony jako pierwszy.

Ajaxowe linki

Poniższy kod Javascript pozwala szybko zamienić istniejące już linki na działające ajaxowo. Proponuję go wrzucić do pliku appliacation.js.

 

Teraz wystarczy dodać do istniejących linków klasę (CSS’ową) get, post, put lub delete w zależności od pożądanej metody wywołania linku, by zmienić je w linki ajaxowe. Polecam do tego plugin LiveQuery, który to zachowanie automatycznie podepnie także do każdego nowego linku opatrzonego którąś z tych klas, który pojawi się na stronie (np. w efekcie działania jakiegoś wywołania ajaxowego). Dodatkowo do naszych linków możemy dodać opcjonalny atrybut ajaxtaget i ustawić jego wartość na selektor CSS’owy wskazujący na element, do którego zostanie załadowany widok zwrócony przez serwer.

Ajaxowe formularze

Do wysyłki formularzy porzez ajax będziemy potrzebować kolejnego pluginu – jQuery Form.

Ajaxowe formularze będą identyfikowane poprzez CSS’ową klasę ‘ajax’.

Modyfikacja strony po wywołaniu ajaxowym

Standardowym sposobem wyświetlania efektów wywołania ajaxowego jest odbieranie, obróbka i umieszczenie tego, co ono zwróci w jakimś miejscu na stronie. Innym sposobem jest umieszczenie całego kodu modyfikującego stronę w odpowiedzi zwracanej przez ajaxowe wywołanie i tylko wykonywanie go przez przeglądarkę. Taką funkcjonalność daje nam kolejny plugin do jQuery – Taconite. Cytując autora pluginu: “Taconite pozwala wykonywać modyfikacji wielu elementów strony naraz, korzystając z odpowiedzi zrwóconej przez pojedyncze wywołanie ajaxowe. Taconite używa do tego XML’owego dokumentu zawierającego instrukcję jak modyfikować stronę”.

Przykładowy widok XML z instrukcjami dla Taconite wygląda tak:

Dzięki podejściu oferowanym przez Taconite możemy poprawić strukturę aplikacji. Weźmy dla przykładu wspomnianą wcześniej funkcjonalność głosowania. Do tej pory kod Javascriptowy, który dbał o wyświetlenie rezultatów oddania głosu (np. zwiększenie countera, usunięcie linku ‘głosuj’, wyświetlenie komunikatu ‘głos został oddany’, itp) musiał być umieszczany na każdej stronie, na której było możliwe oddanie głosu. Teraz jest on po prostu zwracany jako odpowiedź serwera na oddanie głosu, czyli znajduje się tam gdzie być powinien.

Kolejny przykład – wyświetlanie flash messages w odpowiedzi na wywołania ajaxowe. Załóżmy, że to jest fragment layoutu naszej strony, w którym wyświetlamy komunikaty flash[:notice]:

Niech to będzie layout taconite, w którym zagnieżdzamy widoki zwracane na wywołania ajaxowe:

Teraz komunikaty flash[:notice] będą wyświetlane w standardowym miejscu na stronie z efektem fade-in. W podobny sposób możemy aktualizować inne elementy strony, np. wszelakie countery.

A Ty jakich narzędzi używasz?

Jestem ciekaw jakich rozwiązań używacie do implementacji Ajaxa w Waszych aplikacjach. Preferujecie jakieś inne pluginy/biblioteki?

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ą :)

Ulepszanie Railsowego Paperclipa

Sunday, October 26th, 2008

Paperclip to obecnie jeden z najlepszych, o ile nie najlepszy plugin do Ruby on Rails automatyzujący proces uploadu plików na serwer. Potrafi m.in. od razu sam wygenerować miniaturki zapisywanych obrazków, obsługuje także zapis na serwerach S3 Amazona. Każdy, kto go używał na pewno docenia jego zalety i prostotę obsługi, ale myślę też, że wielu natknęło się na sytuacje gdy okazał się on jednak niewystarczający.

Przykładowo, miniaturki obrazków generowane są zawsze w ten sam standardowy sposób co daje zamierzony efekt w większości przypadków, ale gdy chcemy poddać nasz obrazek jakimś dodatkowym transformacjom podczas skalowania, okazuje się to niemożliwe.

Innym przykładem jest obsługa S3 Amazona. Paperclip potrafi tam zapisywać pliki bez żadnych problemów, wystarczy, że podamy nasz API key oraz nazwę bucketa, do którego trafiać mają pliki. Co jednak w sytuacji gdy później na jednej stronie chcemy wyświetlić na raz wiele zuploadowanych zdjęć siedzących w jednym buckecie? Zdjęcia będą się ładować wolno. A to dlatego, że większość przeglądarek ma domyślnie włączone ograniczenie pozwalające w jednej chwili pobierać tylko 2 pliki z jednego hosta, niezależnie od dostępności łącza. Zatem dobrze było by rozmieścić nasze zdjęcia na wielu hostach (bucketach), żeby zwiększyć prędkość pobierania.

Na szczęście dzięki przemyślanej budowie samego pluginu i możliwości łatwego rozszerzania klas i modułów z dowolnego miejsca w języku Ruby nic nie stoi na przeszkodzie, żeby powyższe funkcjonalności dodać samemu.

Postanowiliśmy więc nie czekać na developerów Paperclipa i stworzyliśmy pakiet własnych rozszerzeń – PaperclipExtended. Znajdziecie tam rozwiązania powyżej wymienionych problemów oraz paru mniejszych. Jeśli okaże się, że jeszcze czegoś nam brakuje, na pewno to dodamy.

P.S. Paperclip 3.0 ma zawierać możliwość dowolnego definiowania komend przekształcających obrazki (coś a’la commands w PaperclipExtended) oraz móc skalować pliki video. Ale to dopiero w planach developerów, zanim się tego doczekamy, musimy działać sami :)

SwfUpload a Flash 10

Tuesday, October 21st, 2008

Swfupload to bardzo zgrabne połączenie flasha z javascriptem, które w efekcie daje nam mechanizm uploadu plików na serwer, który możliwościami znacznie wykracza poza standardowy <input type=”file” …/>. Podstawowe zalety, to możliwość uploadu wielu plików naraz i pokazywanie paska postępu całego procesu.

Jednak nie wszystko zawsze działa tak jak należy. Część problemów z używaniem tej biblioteki wynika z różnic pomiędzy kolejnymi wersjami Flasha lub błędów w implementacji niektórych funkcjonalności po stronie Flasha właśnie. Część z tych problemów i metod ich rozwiązania opisałem kiedyś na blogu CodeTunes, ale teraz wyszła nowa wersja Flasha – 10.

Flash 10 podchodzi trochę bardziej restrykcyjnie do niektórych aspektów bezpieczeństwa od swoich poprzedników. Jedną z takich zmian jest zablokowanie wywołania okna dialogowego do wyboru plików spoza flasha, co bezpośrednio uderza w SwfUpload, który robił to z poziomu Javascript.

Na szczęście reakcja developerów SwfUpload’a była dość szybka i rozpoczęli prace nad wersją 2.2 biblioteki, która już radzi sobie z tym problemem. Warto jednak zauważyć, że wersja 2.2, pomimo kompatybilności z Flashem 10 we wspomianym aspekcie, znajduje się jeszcze w fazie alpha, więc może mieć wciąż jakieś niedociągnięcia. Polecam więc upgrade do stabilnej wersji jak się tylko ukaże.

Poniżej instrukcja jak przekonfigurować swoją aplikację do używania SwfUpload w wersji 2.2.

Najpierw ściągamy najnowszą wersję biblioteki z SVN

svn co http://swfupload.googlecode.com/svn/swfupload/trunk/core swfupload

Z tego co sciągniemy potrzebujemy tylko dwa pliki: swfupload/swfupload.js i swfupload/Flash/swufpload.swf. Są to nowe wersje plików, które już powinniśmy mieć w swojej aplikacji, więc podmieniamy stare na nowe.

W nowej wersji SwfUpload sam sobie generuje przycisk otwierający okienko z wyborem plików, my musimy tylko wskazać mu miejsce gdzie ma go umieścić.

<div id="swfuploadButtonPlaceHolder"></div>

Pozostaje nam jeszcze tylko zmiana w opcjach z poziomu Javascript:

swfu = new SWFUpload({
...
button_placeholder_id: "swfuploadButtonPlaceHolder",
button_image_url: "../images/swfupload/button.jpg",
button_width: "216",
button_height: "25"
...});

Ścieżka do “button_image_url” musi być relatywna do lokalizacji pliku swf, a sam obrazek, na który wskazuje powinien wyglądać mniej więcej tak:

Czyli jeden obrazek zawierający 3 stany przycisku (normalny, onmouseover, onclick). Zamiast obrazka możemy podać tekst:

swfu = new SWFUpload({
...
button_text: '<span class="theFont">Upload</span>',
button_text_style: ".theFont { font-size: 16; }"
...});

To wszystko. Działający przykład można obejrzeć tutaj.

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.

Import ticketów do Trac-a z pomocą Ruby, część 2

Tuesday, July 22nd, 2008

Jest to uaktualnienie pomysłu Wiktora na automatyczny i masowy import ticketów do Trac-a. Dla przypomnienia: założenie jest takie, że tickety mamy w pliku tekstowym, gdzie jedna linia = jeden ticket.

Nazwy pól formularza dodawania nowego ticketu zostały zmienione w obecnej wersji Trac-a (0.11), dlatego też trzeba było uaktualinić je również w skrypcie. Przy okazji skrypt został przekształcony w rake task, do którego parametry podaje się z linii poleceń.

Kod pliku znajduje się na pastie.

Użycie:

rake trac:import url=http://path.to.svn/trac.cgi/login user=username
  pass=password owner=username file=tickets.txt

url – adres URL strony logowania do Trac-a
user i pass – dane konta do logowania
owner – komu przypisać ticket
file – plik z ticketami (jedna linia = jeden ticket)

W razie potrzeb łatwo zedytować plik dopisując dodatkowe pola do wypełnienia w formularzu.

Kod korzysta z gema Mechanize. Instalacja: gem install mechanize.

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.