Tag: framework

erstellt von max, am 19.02.2008 19:05

also was verursacht mal probleme bei modulen:

  1. administration der module
  2. namespace (wo werden die module hin-geladen)
  3. ladereihenfolge (wie werden module geladen, module sollen code ueberschreiben/erweitern koennen)
  4. wie wird code aus den modulen, bzw der core aus modulen aus, angesprochen,

zu 1)

das ist mal das leichteste. zum einen koennen ueber das webtek-script module angelegt werden, bzw in den modulen Pages, Models, migrations, etc. erzeugt werden.

./webtek module comments
./webtek --module comments Page Comment
./webtek --module comments migrate create comment_table

# ausfuehren von migrations (u.a. in den modulen tags, comments)
./webtek --modules comments,tags migrate up

zum anderen definiert man in der httpd.conf welche module geladen werden

PerlSetVar module comments,tags

zu 2)

wenn wir jetzt unser Weblog nehmen, dann sind die Perl-Module unter Weblog::Model::Comment, Weblog::Page::Comment usw erreichbar. jetzt wollte ich es so, dass die kommentare unter Weblog::Model::Comment bzw, Weblog::Page::Comment erreichbar sind. das hat den vorteil, dass man noetigenfalls den modul-code nochmal per applikation abaendern/ueberschreiben kann. damit das funktioniert kommen wir mit dem herkoemmlichen:

package Weblog::Model::Comment

...

use Weblog::Model::Comment

nicht mehr aus. darum gibt es jetzt WebTek::Module welches das laden der module uebernimmt. d.h. u.a. das richtige package definiert. aber das wiederum loest noch nicht das use problem, denn auch dort muss magic passieren, um fuer einen package-namen das richtige modul zu laden, und perl waere nich perl wenn man nicht auch das hacken koennte:

my $prefix = app->class_prefix; # liefert in unserem fall 'Weblog'
*CORE::GLOBAL::require = sub {
   my $package = shift;
   my ($pkg, $ret) = ($package, undef);
   if ($pkg =~ /$prefix/) {
      $pkg =~ s/\//::/g;
      $pkg =~ s/.pm$//;
      $ret = WebTek::Module->require($pkg);
   } else {
      $ret = eval { CORE::require($package); 1 } or die $@;
   }
   return $ret;
};

somit wird ein package, welches in unserer Application liegt ueber WebTek::Module geladen, alle anderen ueber den konventionellen weg (file suchen in @INC usw.).

zu 3)

diese funktionalitaet uerbnimmt auch WebTek::Module. wenn wir z.b. das File Model/Comment.pm laden wollen, dann wird dieses einfach ueberall in allen core/modulen gesucht und der reihe nach geladen. somit ist es auch moeglich modul-code in einem anderen (spaeter geladenen modul) wieder zu ueberschreiben (ob das sinnvoll ist weisz ich nicht)

zu 4)

im tags modul gibt es eine Page welche uns alle Items fuer einen tag rendert. wir haetten das gerne unter der url:

/weblog/tag/<tag_name>

normalerweise wuerden wir das so definieren

package Weblog::Page::Tag

use WebTek::Parent qw( Weblog::Page::Root );

sub new_for_tag :Path(tag/(\w+)) {
   my ($class, $path, $tagname) = @_;

   ...
}

...

1;

hier sind aber leider haufenweise absolute module-namen (=schlecht). darum koennen wir jetzt schreiben.

use WebTek::Parent app->Page->Root

sub new_for_tag :Path(tag/(\w+)) {
   my ($class, $path, $tagname) = @_;

   ...
}

...

die package definietion koennen wir uns sparen (uebernimmt WebTek::Module), und alle package-namen koennen wir in der form:

app->Page->Root
app->Model->Tag
...

definieren. weiters zur demo noch z.b. das items-count macro:

sub count :Macro :Param(render the number of items of this tag)
   :Param(disabled='1' count also disabled items for this tag, default 0)
{
   my ($self, %params) = @_;
   
   return app->Model->Tag->count(
      'appname' => app->name,
      'name' => $self->name,
      'is_enabled' => [1, ($params{'disabled'} ? 0 : 1)],
   );
}

damit diese magic funktioniert muss man wieder tief in der perl-trickkiste kramen (vereinfachte from von WebTek::App).

package WebTek::App;

...

our $AUTOLOAD;

sub AUTOLOAD {
   my $name = $AUTOLOAD =~ /::([^:]+)$/ ? $1 : $AUTOLOAD;
   return if $name eq 'DESTROY';
   return WebTek::App::Class->___new___($App->class_prefix . "::$name");
}

package WebTek::App::Class;

use overload '""' => sub { my $self = shift; return $$self };

our $AUTOLOAD;

sub ___new___ {
   my ($class, $name) = @_;
   return bless \$name, $class;
}

sub can { my $self = shift; $$self->can(@_) }
sub isa { my $self = shift; $$self->isa(@_) }
sub DOES { my $self = shift; $$self->DOES(@_) }
sub VERSION { my $self = shift; $$self->VERSION(@_) }

sub AUTOLOAD {
   my $self = shift;
   my $name = $AUTOLOAD =~ /::([^:]+)$/ ? $1 : $AUTOLOAD;
   return if $name eq 'DESTROY';
   return $$self->$name(@_) if UNIVERSAL::can($$self, $name);
   return WebTek::App::Class->___new___("$self\::$name");
}

naja das waeren mal ein paar eckpunkte der module-implementiereung in webtek.

erstellt von max, am 07.05.2008 09:06

so. ich hab jetzt wiedermal einen screencast gemacht, in welchem eine weblogsoftware erstellt wird, die aehnlich dem funktionsumfang von diesem weblog ist. das weblog bekommt folgende features:

  • login/logout
  • artikel erstellen/aendern/loeschen/online-offline stellen
  • suche nach artikeln
  • tags (von tagthe.net) zum artikel speichern + tagcloud in der sidebar
  • kommentare

dabei werden folgende webtek-techniken verwendet:

  • pages/templates (eh klar)
  • events
  • paginator
  • module

soviel funktionalitaet hat auch ihren preis:

  • der screencast dauert 1h 20min (bin gespannt ob sich das wer antut, aber man hat dann wirklich einen guten einblick wie man eine webtek-applikation from scratch erstellt). ich sage am anfang der screencast wird ca. 30-40 min dauern, und wie das halt beim programmieren so ist, dauert immer alles doppelt so lang
  • es ist leider mit einer schlechten audio-quali aufgenommen, deshalb hab ich einen leichten s-sprachfehler ;) und es ist ein bisserl dumpf -> dadurch wirkt die stimme sehr beruhigend (oder auch einschlaefernd...)
  • naja, das video ist fast 200MB gross.

so will es euch nimmer laenger vorenthalten. hier ist der screencast:

http://max.xaok.org/static/weblog/webtek-weblog-demo.mov

den source-code zum nachblaettern findet man unter:

https://max.xaok.org/svn/webtek-apps/Weblog1

und um die applikation laufen zu lassen gehoert das noch ins httpd.conf file.


PerlSwitches -I/WebTek/app
PerlSwitches -I/WebTek/lib
PerlRequire /WebTek/extra/startup.pl

Alias /static/weblog /WebTek/app/Weblog1/static
<Directory /WebTek/app/Weblog1/static>
   Order allow,deny
   Allow from all
</Directory>

<Location /weblog>
   SetHandler modperl
   PerlSetVar name Weblog
   PerlSetVar dir /WebTek/app/Weblog1
   PerlSetVar modules comments
   PerlResponseHandler Weblog1::Handler
</Location>
<Location /weblog/login>
   AuthType Basic
   AuthName "WebTek Weblog Demo"
   AuthBasicProvider file
   AuthUserFile /etc/apache2/auth-files/http.passwd
   Require valid-user
</Location>

etwas kurzweile damit!

erstellt von max, am 16.05.2008 16:06

habe soeben die I18N von properties files auf po files umgestellt. grund dafuer ist, dass po files quasi der standard in I18N sind, und es dafuer schon coole Uebersetzungsprogramme gibt (z.b. poedit). weiters ist gleich eine routine dazugekommen, welche das uebersetzten sehr vereinfachen sollte:

  1. automatische message-key extraktion aus .pm und .tpl files
  2. message-files (= .po files) mit diesen keys erweitern, sofern sie noch nicht vorhanden sind
  3. erstellen von .po files fuer komplett neue sprachen

das alles geht ab jetzt mit dem webtek script, z.b.

./webtek translate de
./webtek translate de en
./webtek translate de en it

extended/erstellt die neotigen po files fuer die angegebenen sprachen. selbstverstaendlich funktioniert das auch in den modulen:

./webtek --modules comments,tags translate en

output:
update file /WebTek/app/Test/messages/en.po with 24 missing keys
update file /WebTek/app/Test/modules/comments/messages/en.po with 4 missing keys
update file /WebTek/app/Test/modules/tags/messages/en.po with 6 missing keys

in den modulen werden natuerlich nur die keys gesucht/erstellt welche auch in dem modul verwendet werden.

zu guter letzt gibt es noch ein helper-script, welches die .properties files in .po files umwandelt:

./webtek script /WebTek/extra/properties_to_po.pl

sollte alles erledigen! der code ist im trunk eingecheckt, und vielleicht noch ein bisserl beta ...

erstellt von max, am 23.05.2008 15:06

hab heut wieder etwas gebastelt und will euch das nicht vorenthalten. zum einen ETag support, zum anderen einen page-cache. weiters will ich hier gleich generell die caching-mechanismen in webtek erlaeutern:

Es gibt folgende arten von cache:

  • macro-cache
  • model-cache
  • page-cache
  • ETag (= caching auf client-seite)

Macro-Cache

um den output eines macros zu cachen, wird einfach das code-attribute Cache angegeben. alternativ kann auch noch eine zeit definiert werden. z.b.

sub posts :Macro :Cache { ... }
sub posts :Macro :Cache(300) { ... } # cached fuer 5min

Model-Cache

dieser cache erlaubt model-suchabfragen zu cachen. dieses gilt aber nur fuer die find_one methode. wenn man z.b. folgende abfrage cachen will:

my $user = app->Model->User->find_one('nickname' => 'max');

dann muss man folgenden code in das User-Model schreiben:

use WebTek::Cache qw( nickname );

folgende definition erlaubt folgende (gecache'te) abfragen:

use WebTek::Cache qw( id nickname street,zipcode );

...

my $user = app->Model->User->find_one('id' => 123);
my $user = app->Model->User->find_one('nickname' => 'max');
my $user = app->Model->User->find_one(
   'street' => $street,
   'zipcode' => $zipcode
);

wenn man das $user object aendert (sprich ein $user->update(%params) macht), wird selbstverstaendlich jeglicher cache geloescht. d.h. bei der naechsten abfrage wird das model neu von der datenbank geholt, und in den cache geschrieben. weiters ist zu beachten, wenn man den protoypen von User aendert (z.b. neue db-column), dass man auch den cache loescht, da dieser sonnst natuerlich noch die alten objekte zurueckliefert!

Page-Cache

heute neu dazugekommen ist der page-cache, welcher es erlaubt gleich ganze seiten zu cachen. hierfuer einfach das Cache attribute (wieder optional mit der zeit, defaultwert ist 1min) bei der action dazufuegen.

sub rss :Action :Cache { ... }
sub rss :Action :Cache(60) { ... }

weiters zu beachten:

  • dieser cache ist hauptsaechlich dafuer da, um peaks auf eine bestimme url zu "verkraften", nicht um die halbe applikation im cache zu halten (obwohl das auch funktioniert)
  • der cache funktioniert nur fuer requests welche ohne einen session->user sind, da meist bei eingeloggten requests die seite viel zu dynamisch ist, um einen cache effektiv arbeiten zu lassen.
  • als (teil vom) cachekey wird die komplette URI verwendet. d.h.

    /weblog/rss
    /weblog/rss?page=2
    

    sind zwei verschiedene cache eintraege.

ETag's

Ein Etag ist ein unique-key fuer eine url, welchen der client bei jedem request dieser url an den server uebermittelt. der server generiert nun fuer diese url seinen eigenen key, und wenn diese zwei uebereinstimmen schickt der server ein http-status 304 (= not modified) retour. damit man diese funktionalitaet in webtek verwenden kann muessen einfach nur die abhaenigkeiten ermittelt werden, wann sich client-cache out-of-date ist. klingt vielleicht kompliziert, ist es aber meist nicht.

z.b. wenn wir ein posting anzeigen wollen:

sub index :Action :ETag($self->post->modify_time) { ... }

reicht meist schon aus, um festzustellen, ob der client noch die aktuelle version im cache hat. man kann auch einfach mehr abhaenigkeiten definieren:

sub index :Action
   :ETag($self->post->modify_time)
   :ETag($self->post->comment_count)
{
    ...
}

Nachtrag

zu guter letzt noch die antwort auf die frage wo webtek daten cached. mit dabei ist schon ein client fuer memcached, welches einfach in der config/cache.config datei aktiviert wird:

{
   #... define the class which should be used for caching
   'class' => 'WebTek::Cache::Memcached',
   
   #... config settings for WebTek::Cache::Memcached
   'WebTek::Cache::Memcached' => {
      'servers' => [ '127.0.0.1:11211' ],
   },

}

man kann auch eigene Cache klassen verwenden solange sie:

  • eine subclasse von WebTek::Cache sind
  • und folgende methoden besitzten
    • new
    • set($key, $value, $expire_time)
    • add($key, $value, $expire_time)
    • get($key)
    • delete($key)
erstellt von max, am 26.05.2008 22:18

so, seit heute unterstuetzt webtek single und concrete table inheritance.

single table inheritance (weitere infos)


create table text (
   id int auto_increment,
   class varchar(100),
   text text,
   comment text,
   PRIMARY KEY (id)
);

man sieht hier, dass es eine column class gibt, welche die model-klasse definiert. hier noch der noetige perl code. wichtig ist, dass die subklassen jeweils in eigenen dateien stehen (sonnst kommt der WebTek::Loader durcheinander). mit den (optionalen) PROPERTIES funktionen kann man die models validieren.

MyApp/Model/Text.pm

use base qw( WebTek::Model );

sub PROPERTIES { 'text' => '.' }

MyApp/Model/Comment.pm

use base qw( MyApp::Model::Text );

sub PROPERTIES { 'comment' => '.' }

nun funktioniert folgender code:

> app->Model->Text->new_default('text'=>'text')->save
Model::Model::Text=HASH(0x....)
> app->Model->Comment->new_default('comment'=>'comment')->save
Model::Model::Comment=HASH(0x....)
> app->Model->Text->find_one(id=>1)
Model::Model::Text=HASH(0x....)
> app->Model->Text->find_one(id=>2)
Model::Model::Comment=HASH(0x....)

concrete table inheritance (weitere infos)


create table posts (
   id int auto_increment,
   text text,
   PRIMARY KEY (id)
);
create table comment (
   id int auto_increment,
   text text,
   PRIMARY KEY (id)
);

hier gibt es zwei tabellen (zusaetzlich muss noch sichergestellt werden, dass diese tabellen nicht die gleichen ids verwenden, entweder auto_increment bei einer tabelle erst bei 1000000 beginnen lassen (danke fuer den tip von adrian), oder irgendwas mit einer sequence). hier wieder der noetige perl code. weiters zu beachten ist, dass man bei den subclassen zusaetzlich zur superklasse (hier z.b. MyApp::Model::Text) noch das WebTek::Model in das @ISA array aufnimmt (sonnst initialisiert der WebTek::Loader das model nicht).

MyApp/Model/Text.pm

sub find_one_factory {
   my $class = shift;
   return app->Model->Post->find_one(@_)
      || app->Model->Comment->find_one(@_);
}
sub find_factory {
   my $class = shift;
   return app->Model->Post->find(@_)
      || app->Model->Comment->find(@_);
}
sub where_factory {
   my $class = shift;
   return app->Model->Post->where(@_)
      || app->Model->Comment->where(@_);
}
sub count_factory {
   my $class = shift;
   return app->Model->Post->count(@_)
      || app->Model->Comment->count(@_);
}
sub count_where_factory {
   my $class = shift;
   return app->Model->Post->count_where(@_)
      || app->Model->Comment->count_where(@_);
}

#... add more general methods
sub foo { ... }
sub text_as_uppercase { ... }
sub whatever { ... }

MyApp/Model/Post.pm

use base qw( MyApp::Model::Text WebTek::Model );

MyApp/Model/Comment.pm

use base qw( MyApp::Model::Text WebTek::Model );

so, dass ist jetzt viel komplizierter, aber man hat dafuer eine saubere trennung der datenbank. und so kann das dann verwendet werden:

> app->Model->Post->new_default('text'=>'text')->save
Model::Model::Post=HASH(0x....)
> app->Model->Comment->new_default('text'=>'comment')->save
Model::Model::Comment=HASH(0x....)
> app->Model->Text->find_one_factory(id=>1)
Model::Model::Post=HASH(0x....)
> app->Model->Text->find_one_factory(id=>2)
Model::Model::Comment=HASH(0x....)
> app->Model->Text->find_one_factory(id=>2)->text_as_uppercase
COMMENT

class table inheritance (weitere infos)

gibts nicht, ist mir zu kompliziert... :)

erstellt von max, am 09.06.2008 10:59

so.. hab jetzt ein mini-test framework in webtek eingebaut:

  • es gibt jetzt ein MyApp/scripts/test verzeichnis
  • in diesem koennen .t files drinnen liegen (gruppierung in unterverzeichnisse ist keine problem)
  • dieses test verzeichnis ist natuerlich auch in jedem modul moeglich
  • ein simples test-file sieht so aus:

    sub init   { }    #... place some code before calling the tests
    sub finish { }    #... place some code after calling the tests
    
    sub sample_test :Test(2) {
       ok(1, "sample test");
       is(2, 2, "sample test2");
    }
    

    • zuerst gibt es die funktionen init und finish. diese funktionen werden in jedem fall zum anfang und zum ende eines test-files ausgefuehrt. diese kann man verwenden um irgendwas zu initialisieren, bzw dann wieder aufzuraeumen (db. usw.)
    • dann gibt es test funktionen. diese werden durch das Test(\d+) attribute deklariert.
    • in diesem attribute muss man angeben, wieviele tests in dieser funktion gemacht werden (also ok, is, like, dies_ok, ... usw aufrufe)
    • alle weiteren funktionen ohne dem attribute werden nicht im rahmen der tests aufgerufen. d.h. diese koennen als hilfsfunktionen fungieren
  • als assertions koennen alle funktionen aus Test::More und Test::Exception verwendet werden.
  • um die tests auszufuehren wird wiedermal das webtek script verwendet. hier der aufruf mitsammt ergebnisliste:

    maxs-macbook:MapToolkit max$ ./webtek test
    
    [info] run testfile /WebTek/app/MapToolkit/scripts/test/Model/X.t
    [info]    - run test 'sample_test' with 2 tests:
    [info] run testfile /WebTek/app/MapToolkit/scripts/test/Page/X.t
    [info]    - run test 'index' with 1 tests:
    [error]      there were some failed tests, look at the details:
    [error]         not ok 1
    [error]            Failed test at /WebTek/app/MapToolkit/scripts/test/Page/X.t line 9.
    [error]                   got: undef
    [error]              expected: 'testvalue'
    
    result: 
     - planned: 3
     - successful: 2
     - failed: 1
    

    man sieht hier, dass ein test fehlerhaft war, und ganz unten ist noch eine zusammenfassung von allen tests in allen test-files.

Controller testen

um einen controller zu testen gibt es die WebTek::Engine::Test Engine. mit dieser kann man den request, response und session initialisieren. ein simpler test sieht dann aus:

sub index :Test(1) {
   app->engine->prepare;
   app->Page->Root->new->index;
   is(response->title, 'WebTek Tests');
}

sub create :Test(4) {
   #... test get
   app->engine->prepare;
   app->Page->Root->new->create;
   is(response->title, 'WebTek Tests - Create new Test');
   #... test post
   app->engine->prepare(
      'method' => 'post',
      'params' => { 'name' => [qw( new test )] },
   );
   thorws_ok { app->Page->Root->new->create } 'WebTek::Exception::Redirect';
   is(response->status, 302);
   #... check created model
   my $test = app->Model->Test->find_one('name' => 'new test');
   isa_ok($test, app->Model->Test);
}

Nachtrag

so.. das war jetzt nur ein kleiner auszug, wie man testen kann. mehr weis ich selber noch nicht, da ich bis jetzt selber noch nicht getestet hab (schande ueber mich).

weiters bin ich jetzt an dem punkt angelangt, andem WebTek feature-complete ist. die aktuelle version ist 0.9.1, und hoffenlich gibt es dann bald eine version 1.0, welche ueber

  • viele Tests
  • und eine gute doku

verfuegt!

erstellt von max, am 10.06.2008 19:18

also in perl gibt es packages:

package MyApp::Model::X;

...

sub x { ... }

soweit so gut. aber in einem modularen Framework kommt man oft mit packagenamen nicht weit, denn diese sind normalerweise in irgendeinem namespace. z.b. gib es in der MyApp application u.a. folgende Packages.

package MyApp::Model::Comment;

...

package MyApp::Model::Text;

use MyApp::Model::Comment;

...


wenn man jetzt aber modular programmiert, hat man optimalerweise die comment-funktionalitaet in einem modul gekapselt, damit man das bei einer anderen applikation auch gleich verwenden kann. nur heisst die andere applikation dann z.b. MyApp2 und somit wird auch das Comment package in dem MyApp2 namespace geladen. d.h. das Text package muesste so definiert sein.

package MyApp2::Model::Text;

use MyApp2::Model::Comment;

damit man jetzt ein modul in mehreren namespaces verwenden kann, gab es in webtek den trick mit folgender definition:

package MyApp2::Model::Text;

use app->Modul->Comment;

das app->Modul->Comment wurde dann fuer die jeweilige applikation in z.b. MyApp2::Module::Comment uebersetzt. diese logik ist aber alles andere als gut, da es dann bei folgendem aufruf:

app->Model->Comment->find_one(id=>123)

keine unterscheidung mehr zwischen dem packetnamen, und der methode die aufgerufen wird, gibt. darum heisst die neue syntax jetzt wie folgt:

package MyApp2::Model::Text;

use app::Model::Comment;

my $comment = app::Model::Comment->find_one(id=>123);

intern wird das app::Model::Comment allerdings weiter in MyApp2::Model::Comment uebersetzt (mittels einem source-filter). d.h. weiters, dass man die module zusatztlich noch in jeder applikation in einem eigenen namespace liegen (= geladen) hat.

ich hoffe es hat jetzt irgendwer verstanden, was ich gemeint hab. wenn nicht, dann bitte nachfragen.

erstellt von max, am 20.06.2008 10:27

hab jetzt grad auf ajaxian einen artikel ueber sproutcore gelesen, und dann auch gleich das tutorial durchgemacht, und ich muss sagen. ich bin begeistert. man kann wie gewohnt eine MVC applikation entwickeln, die dann aber client-seitig laeuft, und die verbindung zum backend passiert einfach ueber JsonRPC. das ganze erinnert mich sehr an das extjs framework, nur dass das look&feel eher an eine apple applikation erinnert!

so denn, ich rate allen man das tutorial durchzumachen, denn u.a. fuer admin-tools ist das ein wirklich tolles framework.