Wiki à la Yii
Ebben a rövidke leírásban azt szeretnénk bemutatni, milyen egyszerű elkészíteni egy elegáns webes alkalmazást a Yii keretrendszer segítségével. Ha a későbbiekben igény lesz rá, természetesen belevághatunk kényesebb témákba is, mint például a biztonság, gyorsítótárazás, komolyabb Active Record használat stb.
Bevezetés
Fogalmak
Itt felsorolnék néhány alapvető (és igen fontos) fogalmat, amelyek ismerete szükséges:
- Objektumorientált programozás (PHP 5)
- A jelenleg stabil Yii 1.1-es verzió a PHP 5.1-en alapszik, tehát annak és objektumorientált programozási lehetőségeinek ismerete mindenképp szükséges.
- Relációs adatbázis (SQL)
- A Yii sokféle relációs adatbázist támogat. A példában én az SQLite-ot választottam, mert nem igényel különösebben bonyolult beállításokat.
- MVC (Model–View–Controller) avagy Modell–Nézet–Vezérlő
- A Yii egy MVC mintát szorosan követő rendszer, ez dióhéjban annyit jelent, hogy az alkalmazás jól elkülöníthető három részre: A modellre, ami az adatbázis-lekérdezéseket végzi, a view-ra vagy nézetre, ami a megjelenítésről gondoskodik, s végül a controller-re vagy vezérlőre, amely kapcsolatot teremt az előző kettő között, s feldolgozza a felhasználó által bevitt információt.
- Active Record
- Az Active Record minta megszünteti alkalmazásunk függőséget a különböző adatbázisoktól, a táblákat és sorokat objektumokként kezeli. Ez a gyakorlatban azt jelenti, hogy mondjuk fejlesztési környezetben használhatunk SQLite-ot, élesben pedig MySQL-t vagy MSSQL-t.
Környezet
A programot a következő konfigurációval készítettük:
- Ubuntu Linux (9.10)
- Apache 2.x
- SQLite 3
- PHP 5.2.x
- Yii 1.1.x
Csomagoljuk ki a letöltött Yiit a web szerverünk főkönyvtárába (pl. /var/www/yii/
). A forráskód majd szintén a /var/www/
alá kerül (pl. /var/www/yiiki/
).
Az is fontos, hogy a Yii rendszer a parancssorból is futtatható legyen! (PHP CLI)
Az adatbázis felépítése
CREATE TABLE pages (
id INTEGER PRIMARY KEY,
title VARCHAR(125),
body TEXT,
revision INTEGER,
created INTEGER
);
Az alkalmazás létrehozása
Az alap alkalmazás létrehozása rendkívül egyszerű, csak adjuk ki a kovetkező utasítást (a webszerver főkönyvtárában állva):
imehesz@imehesz-laptop:/var/www$ ./yii/framework/yiic webapp yiiki
Create a Web application under '/var/www/yiiki'? [Yes|No] yes
mkdir /var/www/yiiki
mkdir /var/www/yiiki/themes
mkdir /var/www/yiiki/themes/classic
mkdir /var/www/yiiki/themes/classic/views
mkdir /var/www/yiiki/themes/classic/views/layouts
mkdir /var/www/yiiki/themes/classic/views/system
generate themes/classic/views/.htaccess
mkdir /var/www/yiiki/themes/classic/views/site
generate index.php
mkdir /var/www/yiiki/css
generate css/bg.gif
generate css/form.css
generate css/screen.css
generate css/main.css
generate css/ie.css
generate css/print.css
mkdir /var/www/yiiki/assets
mkdir /var/www/yiiki/images
mkdir /var/www/yiiki/protected
mkdir /var/www/yiiki/protected/extensions
generate protected/yiic.bat
mkdir /var/www/yiiki/protected/models
generate protected/models/LoginForm.php
generate protected/models/ContactForm.php
generate protected/yiic.php
mkdir /var/www/yiiki/protected/data
generate protected/data/schema.sqlite.sql
generate protected/data/schema.mysql.sql
generate protected/data/testdrive.db
mkdir /var/www/yiiki/protected/controllers
generate protected/controllers/SiteController.php
generate protected/yiic
mkdir /var/www/yiiki/protected/config
generate protected/config/main.php
generate protected/config/test.php
generate protected/config/console.php
mkdir /var/www/yiiki/protected/commands
mkdir /var/www/yiiki/protected/commands/shell
mkdir /var/www/yiiki/protected/messages
generate protected/.htaccess
mkdir /var/www/yiiki/protected/views
mkdir /var/www/yiiki/protected/views/layouts
generate protected/views/layouts/main.php
mkdir /var/www/yiiki/protected/views/site
generate protected/views/site/login.php
mkdir /var/www/yiiki/protected/views/site/pages
generate protected/views/site/pages/about.php
generate protected/views/site/index.php
generate protected/views/site/contact.php
generate protected/views/site/error.php
mkdir /var/www/yiiki/protected/runtime
mkdir /var/www/yiiki/protected/tests
generate protected/tests/WebTestCase.php
generate protected/tests/phpunit.xml
generate protected/tests/bootstrap.php
mkdir /var/www/yiiki/protected/tests/report
mkdir /var/www/yiiki/protected/tests/functional
generate protected/tests/functional/SiteTest.php
mkdir /var/www/yiiki/protected/tests/fixtures
mkdir /var/www/yiiki/protected/tests/unit
mkdir /var/www/yiiki/protected/components
generate protected/components/Controller.php
generate protected/components/UserIdentity.php
generate index-test.php
Your application has been created successfully under /var/www/yiiki.
Ha minden jól sikerült, akkor a Your application has been created successfully under /var/www/yiiki üzenet jelenik meg, ami röviden annyit jelent, hogy az alap program sikeresen létrehozva a megadott mappa alatt.
Azt már itt érdemes megjegyezni, hogy a programunk már ebben a fázisban működőképes. Ha megtekintjük a URL-t egy böngészőben (pl. http://localhost/yiiki
) akkor (remélhetőleg) egy üdvözlőképernyő fogad bennünket.
Az alkalmazás összekapcsolása az adatbázissal
A wikik többnyire nagyon egyszerű, mondhatnánk butácska programok, amik semmi mást nem csinálnak, csak oldalakat tárolnak és mutatnak meg. A problémát többféleképpen is meg lehetne oldani, még adatbázis használata nélkül is! De mi szeretnénk bemutatni, hogy mennyire egyszerű adatbázisokat kezelni Yiivel.
A fő konfigurációs állomány a protected/config/
mappa alatt található, és main.php
a neve. Nyissuk is meg a kedvenc szövegszerkesztőnkkel! (Mármint amelyiket programozásra használunk). Aki egyből a vimre gondolt, az kap egy piros pontot.
Módosítsuk az alábbiak szerint (fontos: az adatbázis állományt a protected/data/
alá tettük):
'db' => array(
'connectionString' => 'sqlite:protected/data/yiiki.sqlite',
),
És ennyi.
A yiic
, avagy a Yii varázspálcája
Természetesen lehetne a fejlesztést a yiic
teljes hanyagolásával folytatni, de használata mindenképpen megkönnyíti a programozást. A __ROOT__
könyvtárunkban egyszerűen adjuk ki a következő utasítást:
imehesz@imehesz-laptop:/var/www/yiiki$ ./protected/yiic shell
Yii Interactive Tool v1.1 (based on Yii v1.1.0)
Please type 'help' for help. Type 'exit' to quit.
>> _
Ha járatosak vagyunk valamelyik adatbáziskezelő parancssori programjával, akkor ez ismerős lehet. Segítséget a help [utasitas]
-sal lehet kérni. Vágjunk is bele.
A Pages
modell létrehozása a yiic
segítségével
Egyszerűen adjuk ki a következő parancsot (még mindig a shellben vagyunk!):
>> model Page pages
generate models/Page.php
generate fixtures/pages.php
generate unit/PageTest.php
The following model classes are successfully generated:
Page
If you have a 'db' database connection, you can test these models now with:
$model=Page::model()->find();
print_r($model);
>>
Nos, itt elég sok minden történt, szűrjük ki, ami nekünk fontos lehet.
model/Page.php
– ez nagyon fontos, ez lesz a modellünk, ami majd az adatbázis-lekérdezéseket végzi.
fixtures/pages.php
unit/PageTest.php
Sajnos ez utóbbi kettővel nem fogunk foglalkozni (legalábbis ebben a cikkben nem), annyit érdemes megjegyezni, hogy az egységteszteléshez nyújt segítséget.
A parancs lefutása után a program arról is értesít minket, hogy ha be van állítva az adatbázis kapcsolatunk, akkor ki is próbálhatjuk. Egyelőre tudjuk, hogy semmi nincs az adatbázisunkban, szóval nem sok értelme lenne a lekérdezésnek.
Mi is az a CRUD?
Azzal, hogy elkészítettük a Page
modellünket, még semmi böngészőben látható eredményt nem sikerült produkálnunk. No és itt jön a híres CRUD (create, read, update, delete) a képbe. Ezeket az utasításokat az adatbázisunk (pages
táblánk) egy-egy rekordján fogjuk alkalmazni.
A vezérlő és nézet létrehozása a crud
paranccsal
Még mindig a shellben állva adjuk ki a következő parancsot:
>> crud Page
generate PageController.php
generate PageTest.php
mkdir /var/www/yiiki/protected/views/page
generate create.php
generate update.php
generate index.php
generate view.php
generate admin.php
generate _form.php
generate _view.php
Crud 'page' has been successfully created. You may access it via:
http://hostname/path/to/index.php?r=page
>>
Aha, itt már lényegesen több minden történt mint a modell létrehozásánál. Láthatjuk, hogy maga a vezérlő is elkészült, ezen felül egy újabb tesztfájl, a view/page/
mappa is, ahol a rendszer fogja tárolni a nézeteket (create.php
, update.php
, view.php
stb).
Megjegyzés: a vezérlő külön is elkészíthető a controller page
paranccsal.
Gyors áttekintés
Programunk jelenlegi állapotában mindenre kész. Tudunk oldalakat listázni, készíteni és törölni. Hogy megbizonyosodjunk róla, böngészőnkkel látogassuk meg a http://localhost/yiiki/index.php?r=page
URL-t.
Na de mi nem elégszünk meg ennyivel, hiszen egy igazi wikit szeretnénk készíteni. Jöhet a testreszabás.
Alkalmazásunk testreszabása
A beviteli űrlap módosítása
Ha most probálnánk egy oldalt készíteni (http://localhost/yiiki/index.php?r=page/create
), akkor azonnal észlelnénk, hogy a beviteli űrlaponon egy csomó olyan mező van, amit nem szeretnénk minden egyes alkalommal kézzel kitölteni, illetve elvárnánk a programunktól, hogy saját maga töltse ki. Ilyen például a created vagy a revision mező.
Módosítsuk az űrlapunkat (protected/views/page/_form.php
) az alábbiak szerint:
<div class="form">
<?php echo CHtml::beginForm(); ?>
<p class="note">Fields with <span class="required">*</span> are required.</p>
<?php echo CHtml::errorSummary($model); ?>
<div class="row">
<?php echo CHtml::activeLabelEx($model, 'title'); ?>
<?php echo CHtml::activeTextField($model, 'title', array('size' => 60, 'maxlength' => 125)); ?>
<?php echo CHtml::error($model, 'title'); ?>
</div>
<div class="row">
<?php echo CHtml::activeLabelEx($model, 'body'); ?>
<?php echo CHtml::activeTextArea($model, 'body', array('rows' => 6, 'cols' => 50)); ?>
<?php echo CHtml::error($model, 'body'); ?>
</div>
<div class="row buttons">
<?php echo CHtml::submitButton($model->isNewRecord ? 'Create' : 'Save'); ?>
</div>
<?php echo CHtml::endForm(); ?>
</div><!-- form -->
A Page
modell szabályai
A modellünkre (protected/models/Page.php
) is ráfér egy kis farigcsálás. A yiic
többnyire kitalálta, hogy milyen szabályokat hozzon létre a különböző mezőkhöz, de a gondolatolvasástól azért még messze van. Mi ugyanis azt szeretnénk, hogy az oldalunk címében csakis betűk, számok vagy aláhúzás jel szerepelhessen. (Egy jó kis bővítés lenne, ha a programunk automatikusan felülírná a bevitt a címet, de az egyszerűség kedvéért itt egy szabályt hozunk létre.)
/**
* @return array validation rules for model attributes.
*/
public function rules()
{
// NOTE: you should only define rules for those attributes that
// will receive user inputs.
return array(
array('created, revision', 'numerical', 'integerOnly' => true),
array('title', 'length', 'max' => 125),
array('title', 'required', 'message' => 'A cím nem lehet üres!'),
array('title', 'unique'),
array(
'title',
'match',
'pattern' => '/^[A-Za-z0-9_]+$/',
'message' => 'Csak számokat, betűket és „_” jelet használhatsz! Hehe.'
),
array('body', 'safe'),
);
}
A használt szabályok elég egyértelműek, de ha valaki jobban bele szeretne mélyülni itt olvashat róluk bővebben: http://yiiframework.com/doc/cookbook/56. Itt azt is megfigyelhetjük, hogy hogyan kell a programunkat magyarul tanítani.
A beforeSave()
és save()
tagfüggvények használata/felülírása
A modelljeink, amik a programunkban az adatbázis-lekérdezéseket kezelik, mindenféle tagfüggyvényekkel fel vannak fegyverezve, hogy nekünk ne kelljen védőkesztyű nélkül SQL-kódban kotorászni. Ilyen tagfüggvény a save()
és hű csatlósa a beforeSave()
.
A pages
táblánkban van egy olyan mező, hogy created
, dátum típussal (a példánkban Unix időbélyeget használunk). Ez a mező tartja nyilván, hogy az adott oldal mikor készült/frissült. Ugyan használhatnánk a save()
tagfüggvényt arra, hogy ezt az értéket beállítsuk, a példa kedvéért ezt a beforeSave()
-vel oldjuk meg. (Az fontos, hogy itt a szülő beforeSave()
-jével térjünk vissza!)
public function beforeSave()
{
// and setting the created date...
$this->created = time();
return parent::beforeSave();
}
A save()
-nél már kicsit más a helyzet. Először vegyük sorra, hogy mit is szeretnénk a programunktól, amikor elmentünk egy oldalt. A jövőben szeretnénk egy revert opciót, ami annyit jelent, hogy az odalalainkat, ha akarjuk, visszaállíthatjuk egy korábbi állapotba. Ezt többféle módon is meg lehet valósítani, de talán a legegyszerűbb megoldás, ha az oldalakat mindig újként mentjük el, és csak növeljük a revision-t azaz verziószámot.
public function save($validate = true)
{
if ($this->isNewRecord) {
// we increase the revision number...
$this->revision = $this->revision + 1;
return parent::save($validate);
} else {
// by setting `save` to false, it will skip the validation,
// so we can save the page with the same title
// also, update is not really an update because every single change
// will be a "new" page, so we can keep history...
$newpage = new Page();
$newpage->attributes = $this->attributes;
$newpage->save(false);
return true;
}
}
A Yii olyan finomságokkal lát el bennünket, mint például a $this->isNewRecord
, ami egyszerűen megmondja nekünk, hogy most egy új rekordról van-e szó, vagy csak felülírunk egyet. Mi persze tudjuk, hogy nekünk arra van szükségünk, hogy minden egyes oldal újként legyen elmentve, tehát ha az oldal nem új, akkor egyszerűen gyártunk egyet, mely a $newpage->attributes = $this->attributes
miatt felveszi az eredeti oldal értékeit.
Még annyit érdemes megjegyezni, hogy ha a save()
tagfüggvényt false
paraméterrel hívjuk meg, akkor az Active Record szabályai nem vonatkoznak az adott rekordra.
Az oldalakat lekérő SQL módosítása, és a hozzá tartozó nézet testreszabása
Ha most néznénk meg az oldalak listáját, azt vennénk észre, hogy programunk az egyes oldalakhoz tartozó összes verziót megmutatja. Ez idáig rendben is van, hiszen minden egyes változatot újként mentettünk el, azonban ez elég zavaró lehet, meg bugyután is néz ki. Ha eddig még nem említettük volna, majd minden vezérlőnek van egy actionIndex()
-e, az alapértelmezett tagfüggvény. Jelen esetünkben ez szolgáltatja az oldalak listáját. Keressük is meg a PageController
-ben (protected/controllers/PageController.php
), és módosítsuk a következők szerint:
/**
* Lists all models.
*/
public function actionIndex()
{
$criteria = new CDbCriteria();
$criteria->group = 'title';
$criteria->order = 'created DESC';
$dataProvider = new CActiveDataProvider('Page', array(
'pagination' => array('pageSize' => self::PAGE_SIZE),
'criteria' => $criteria,
));
$this->render('index', array('dataProvider' => $dataProvider));
}
SQL-ben valamennyire jártasabbak egyből kiszúrhatják, hogy mi is történik. Itt létrehozunk egy CDBCriteria()
osztályt, melynek segítségével különböző SQL paramétereket állíthatunk be, és ezáltal módosíthatjuk a lekérdezéseinket, ami ebben az esetben az oldalak csoportosítását jelenti a cím szerint és rendezést a létrehozás dátuma szerint.
Az alábbi állományokat pedig módosítsuk ízlésünk szerint (illetve az én ízlésem szerint). Ha még emlékszünk, a PageController
-t kicsit átalakítottuk, hogy az adott oldal azonosítója helyett a title
mező értékével dolgozzon. Ez nagyon szép és jó, de a program többi részét is kicsit át kell alakítanunk. Ilyen például az actionCreate()
és actionUpdate()
metódusokban használt redirect()
, mely azt a célt szolgálja, hogy bizonyos események után böngészőnket egy meghatározott oldalra irányíthassuk.
public function actionCreate()
{
$model = new Page;
if (isset($_POST['Page'])) {
$model->attributes = $_POST['Page'];
if ($model->save())
$this->redirect(array('view', 'title' => $model->title));
// $this->redirect(array('view', 'id' => $model->id));
}
$this->render('create', array('model' => $model));
}
A következő állományok (protected/views/page/index.php
, protected/views/page/view.php
) pedig a megjelenítést szolgálják.
<div class="view">
<?php echo CHtml::link(CHtml::encode($data->title), $this->createUrl('page/view', array('title' => $data->title))); ?>
(Rev.: <?php echo CHtml::encode($data->revision); ?>)
<i><?php echo date('Y-m-d H:i', CHtml::encode($data->created)); ?></i>
</div>
<?php
$this->breadcrumbs = array(
'Pages' => array('index'),
$model->title,
);
?>
<h1><?php echo strtoupper($model->title); ?></h1>
<div>
<?php echo CHtml::link('Oldalak listája', array('index')); ?>
<?php echo CHtml::link('Oldal frissítése', array('update', 'id' => $model->id)); ?>
<?php echo CHtml::linkButton('Oldal törlése', array(
'submit' => array('delete', 'id' => $model->id),
'confirm' => 'Biztos, hogy töröljem?'
)); ?>
</div>
<div><?php echo $model->body; ?></div>
Jajj de jó, már lassan készen is vagyunk. Mondjuk a megjelenített oldal egyáltalán nem úgy néz ki mint egy wiki oldal. A következőkben megnézzük, hogyan lehet külső modulokat behúzni a Yii rendszerbe, pl. egy wiki markup értelmezőt, hogy megússzuk a sok str_replace()
-t.
Külső modulok, kiterjesztések használata
Fejlesztés közben gyakran fordul elő olyan helyzet, hogy valaki (lehet, hogy mi magunk) valahol máshol (pl. egy másik keretrendszer alatt) már megoldotta a felmerülő problémát, és szeretnénk a kódot újra használni. Erre Yii barátunk többféle lehetőséget ad. Használhatunk kifejezetten Yiihez szánt kiterjeszéseket vagy a Yiitől teljesen független csomagokat. Ilyen például a Zend Framework RSS komponense. Nekünk jelenleg erre nincs szükségünk, de ha valakit bővebben érdekel, az utánajárhat.
Már nagyon közel vagyunk ahhoz, hogy egy használható kis wikink legyen, már csak maga a wikinyelv-értelmező hiányzik. Persze írhatnánk egyet magunk, de inkább bzzunk benne, hogy valaki más már megírta – és láss csodát, így is van. (Ez a rendszer a WikiCreole nyelvet használja.) Persze nem kötelező ezt használni, de az egyszerűség kedvéért mi ezt választottuk.
Mint bizonyára észrevettük a URL-ből, ennek a csomagnak az ég világon semmi köze nincs a Yii-hez, ez csak egy egyszerű PHP osztály, tehát nekünk a második megközelítés lesz a legjobb. Töltsük is le az összecsomagolt állományt, és másoljuk be a creole.php
-t az application/vendors/
mappába (ha nincs ilyen mappánk, csináljunk egyet).
Attól még, hogy az állomány ott heverészik a vendors/
könyvárban, a Yii még nem tudja, hogy mi ezt használni is szeretnénk. Itt jön a Yii::import()
statikus tagfüggvény a képbe.
Yii::import('application.vendors.*');
Ami kb. annyit jelent, hogy a továbbiakban Yii rendszerünk ebben a könyvtárban is keresgélni fog PHP osztályok után. (Ezt egyebként beállíthatjuk a protected/config/main.php
állományban is az import
tömb segítségével.)
Most már csak annyit kell tennünk, hogy a megfelelő nézetben meghívjuk magát a wiki értelmezőt, és átpasszoljuk neki azt a szöveget amit HTML-ként szeretnénk megmutatni a felhasználóknak. Ez a nézet pedig a protected/views/page/view.php
alatt található:
$wiki = new creole();
echo $wiki->parse($model->body);
Ja, még valami
Már csak egy apróság van hátra. A rendszer alapból a SiteController
-nek adja át a vezérlést, ha valaki meglátogatja az oldalunkat. Ez szép és jó, de sajnos azon az oldalon csak annyi szerepel, „Welcome to My Web Application” és semmilyen link nem utal arra, hogy hol is vannak az oldalaink. Ezt a legegyszerűbben úgy lehet orvosolni, hogy megváltoztatjuk az alapértelmezett vezérlőt, és a SiteController
helyett beállítjuk a PageController
-t, hiszen az felelős az oldalak kezeléséért. Ja, és ha már úgyis a konfigurációs állományban kell kotorásznunk, akkor be is állíthatjuk a programunk nevét (ami több más helyen is feltűnik, pl. a böngésző címsorában).
return array(
...
'name' => 'Yiiki – Wiki Yiivel',
'defaultController' => 'Page',
...
);
No, készen is lennénk. Persze nagyon NAGYON sok mindenen lehetne javítani-szépíteni, pl. biztonság / adatbázis védelem, magyarosítás, egységtesztek használata, gyorsítótárazás stb., de talán majd máskor.
Kezdetnek reméljük jó lesz. Sok sikert!
Linkek
- A Yii Framework oldal: http://yiiframework.com
- A Yii Radiio oldal, ami a legújabb Yiivel kapcsolatos finomságokat foglalja össze: http://yiiradiio.mehesz.net
- Magyar fórum: http://yiiframework.com/forum/index.php?/forum/33-hungarian
Apró hiba
Azt hiszem, hogy egy apró logikai hiba maradt a save() függvényben: kimaradt egy tagadás az első feltételből:
Fordított logika
$this->isNewRecord
, ami egyszerűen megmondja nekünk, hogy most egy új rekordról van-e szó, vagy csak felülírunk egyet. Mi persze tudjuk, hogy nekünk arra van szükségünk, hogy minden egyes oldal újként legyen elmentve, tehát ha az oldal nem új, akkor egyszerűen gyártunk egyet […]Még nem kerek...
Jöhet még Yii
Csak így tovább! :)
kösz
a cél az (lenne), hogy kisebb (de használható) alkalmazásokon keresztül mutassuk be a Yii rendszert.
az igaz, hogy ha az ember végig meg a demo (blog) programon, akkor elég sok mindent megtanul, de akad olyan eset, ahol az ott bemutatott megoldások nem megfelelőek.
köszi mégegyszer,
--iM