Door
Ozzie PHP
op 30-12-2010 16:10
gewijzigd op 06-01-2011 13:17
50.699 views
Hmmm, laat ik de vraag toch maar eens stellen. Ik wil graag een eigen framework / beheersysteem maken. De bedoeling is dat ik als het systeem klaar is heel makkelijk een website kan maken waar meteen al een standaard cms gedeelte in zit.
Ik ben al begonnen met een framework en ik maak daarbij gebruik van Zend Framework, maar nu vraag ik me het volgende af. Ik heb behoorlijk wat PHP kennis en ervaring inmiddels, maar ik heb hier geen opleiding voor gehad. Ik wil het mezelf dan ook altijd zo makkelijk mogelijk maken als ik aan het programmeren ben. Voorbeeld, als ik een databasequery wil uitvoeren dan wil ik niet een hele query in te hoeven typen, maar wil ik simpele functies kunnen gebruiken, bijvoorbeeld: $database->setTable('tabel') en $row = $database->select('naam') etc.
Ook vind ik het handig dat ik in Zend Framework een route makkelijk kan koppelen aan een controller en een actie. Daarnaast gebruik ik de MVC structuur (modules), de Zend_Registry functie om iets op te slaan en gebruik ik de caching functie voor het cachen van gegevens.
Ik gebruik Zend Framework dus voornamelijk voor:
- maken van mooie routes
- routes koppelen aan controller en actie
- MVC structuur (modules)
- Zend_Registry om variabelen op te slaan
- Caching
Voor de rest gebruik in Zend Framework eigenlijk niet. Ik weet dat er heeeel veel mogelijkheden in Zend Framework zitten, maar ik ben niet iemand die dat allemaal wil uitvogelen, en ik wil toch altijd graag mijn eigen code schrijven zodat ik precies weet wat de code doet en hoe deze in elkaar zit (zodat het voor mijzelf logisch is en makkelijk te gebruiken).
Nu vraag ik me 2 dingen af:
1) is het voor mij eigenlijk wel zinvol om Zend Framework te gebruiken aagezien ik er niet heel veel mogelijkheden van benut.
2) zijn de 5 functies waar ik gebruik van maak (makkelijk) ook zelf te maken of is dat heel erg ingewikkeld?
Wat raden jullie aan? Zend Framework blijven gebruiken ook al gebruik ik er maar weinig van? Of toch zelf mijn eigen functies maken en Zend Framework niet meer gebruiken? Ik stel deze vraag ook omdat Zend Framework zo'n 23mb aan serverruimte in beslag neemt.
Dit zeg je haast elke post :) Je kan het gewoon aanleren het is niet iets wat je hokuspokus erin gepompt krijgt :) Wat dacht je hoe een leraar het doet?
Precies, daarom probeer ik door het telkens te herhalen, net als een leraar, bij jullie erin te pompen dat mijn kennisniveua op het gebied van DI nog niet zo hoog is als dat van jullie ;-)
Dankjewel voor die link nogmaals, maar ik had gehoopt dat iemand op basis van mijn voorbeeldje van de webshop kan aangeven wat je met DI kunt doen. Dit is voor mij een stuk makkelijker te begrijpen omdat ik daar wat meer in thuis ben. Ik denk dat dan het kwartje pas echt gaat vallen. Maar goed, ik kan me ook voorstellen dat je geen zin hebt om dat uit te werken hoor :)
Heel simpel (en niet al te slim) voorbeeldje. Zie het verschil tussen wel en niet vooraf een container maken die je objecten voor je opbouwt. Deze ene controller wordt er een heel stuk simpeler door, en bedenk dat je aardig wat van deze controllers gaat schrijven.
<?php
// dit hele stuk is in weze je config bestand.
$container; // dat daar is mijn container
$container->add('pdo', function() {
return new PDO('...');
})
$container->add('database', function($c) {
return new Database($c->pdo);
});
$container->add('winkelwagentje', function($c) {
return new Winkelwagentje($c->sessie, $c->producten);
});
$container->add('producten', function($c) {
return new Producten_Store($c->database);
});
if (SITE_ENV == 'development')
{
// In m'n test-omgeving zijn PHP's sessies niet betrouwbaar omdat er allemaal sites op hetzelfde domein zitten,
// en dus allemaal dezelfde sessie delen wat gewoon raar is. Dus terugvallen op een speciale database sessie.
// (niet echt, maar het is een excuus om een if-then-else in de configuratie te laten zien)
$container->add('sessie', function($c) {
return new Sessie_Database($c->database);
});
}
else
{
$container->add('sessie', function() {
return Sessie_PHP::getInstance(); // hey, een singleton! Ja, want meerdere Sessies die allemaal gebruik maken van $_SESSION is vreemd, dus er kan maar één native sessie zijn, en dat is deze.
});
}
// tot hier. Einde configuratie.
abstract class Controller
{
protected $container;
public function __construct($container)
{
$this->container = $container;
}
}
// bedenk wel dat dat wat hierboven staat maar één keer moet
class Controller_Winkelwagen extends Controller
{
public function actionAddProduct()
{
$product = $this->container->producten->get($_POST['product_id']);
echo 'In je winkelwagentje zit nu:';
foreach ($winkelwagentje->producten() as $product)
echo $product->naam() . '<br>';
}
}
?>
En nu weer even zonder DI. Niet helemaal eerlijk, het is hier nu veel kleiner maar dit moet je dan op iedere plek waar je deze objecten nodig hebt herhalen en onthouden, en als je het wilt aanpassen of dynamisch wilt maken (wat ik nu heb gedaan met de sessie class) is het helemaal niet meer bij te houden.
<?php
class Controller_Winkelwagen extends Controller
{
public function actionAddProduct()
{
$pdo = new PDO(...);
$db = new Database($pdo);
if (SITE_ENV == 'development')
$sessie = new Sessie_Database($db);
else
$sessie = Sessie_PHP::getInstance();
$producten = new Producten_Store($db);
$winkelwagentje = new Winkelwagentje($sessie, $producten);
// diezelfde initialisatie, maar dan moet dat overal?
$product = $producten->get($_POST['product_id']);
$winkelwagentje->addProduct($product);
echo 'Jeej, product toegevoegd :D';
echo 'In je winkelwagentje zit nu:';
foreach ($winkelwagentje->producten() as $product)
echo $product->naam() . '<br>';
}
}
Als je nu een framework zou maken, dan zou het configuratie gedeelte dus eigenlijk een onderdeel zijn van je applicatie? Correct?
Nog een paar vragen over dit stukje:
<?php
$container->add('pdo', function() {
return new PDO('...');
})
$container->add('database', function($c) {
return new Database($c->pdo);
});
$container->add('winkelwagentje', function($c) {
return new Winkelwagentje($c->sessie, $c->producten);
});
$container->add('producten', function($c) {
return new Producten_Store($c->database);
});
?>
Zou je me kunnen uitleggen wat je hier (ongeveer) doet? Eerst voeg je 'pdo' toe aan de container. Wat is dan de inhoud van 'pdo'? Vervolgens voeg je 'database' toe aan de container met daarin 'pdo'? Wat houdt function($c) precies in?
Als je nu $container->database aanroept krijg je dan telkens een new Database terug? Of is dat telkens dezelfde?
Thanks voor je code tot zover... heb het gevoel dat ik weer een stapje verder ben :)
$container->add($name, $factory) vertelt aan $container dat als hij ooit een instantie van $name moet maken, hij dat $factory moet aanroepen, en die doet dat dan. $container onthoudt dan die instantie voor als je hem daarna nog een keer nodig hebt.
<?php
$container->add('database', 'bouw_mijn_database');
// er gebeurt niets bijzonders
$x = $container->database;
// daaar pas wordt bouw_mijn_database() aangeroepen, omdat $container een instantie van database nodig heeft
$y = $container->database;
// $container heeft al een instantie van database, en hergebruikt die. $x === $y dus.
?>
In de posts hiervoor gebruikte ik een feature uit PHP 5.3, anonieme functies. Maar je kan ook gewoon de functienaam in een string er neerzetten.
Hoe $container precies is geïmplementeerd staat een paar pagina's terug. Ik heb mijn variant ervan daar ergens gepost, en Pimple, die ongeveer hetzelfde werkt, is ook al een paar keer voorbij gekomen. Die $c verwijst weer terug naar de $container, zodat de factory functies, die een instantie moeten bouwen, ook weer diezelfde container kunnen gebruiken om instanties van andere classes op te vragen. Dat $container->database werkt komt door de magic method __get, je zou ook $container->__get('database') ipv $container->database kunnen schrijven.
PDO is gewoon PDO, een database abstraction layer van PHP. Je kan natuurlijk ook MySQLi oid gebruiken als je dat makkelijker vindt. De class Database gebruik ik zelf graag om PDO heen, PDO heeft alleen maar PDO::prepare($sql) en PDO::query($sql), waarin je uitgebreid SQL moet schrijven. Voor inserts en updates is me dat teveel typwerk en Database::insert($table, array $fields) is dan makkelijker. Ook niet noodzakelijk of perce goed OOP, maar ik vind het wel praktisch en ik had wat vulling nodig voor mijn voorbeeld. (Een voorbeeld van hoe je die Database class zou kunnen implementeren staat in dit gastenboek voorbeeld)
Zoiets zou je ook kunnen maken, maar dat wordt een stuk ingewikkelder om daadwerkelijk te scripten omdat je niet zomaar een constructor met een variabel setje argumenten kan aanroepen, daar heb je dan weer ReflectionClass voor nodig. En je moet gaan onderscheiden of 'pdo' gewoon een scalar waarde is, of een verwijzing naar iets in de $container. Als laatst is het minder makkelijk omdat je niet even een functie over een argument kan halen. Geen idee of dat vaak zal voorkomen, maar wanneer het voorkomt gaat het niet.
Die verwijzing zou je nog zo kunnen oplossen, waarbij $container->reference($naam) een of andere speciale waarde teruggeeft die hij weer kan herkennen wanneer hij Database gaat bouwen:
$container->add('database', 'Database', $container->reference('pdo'))
Ik denk eigenlijk dat dan terugvallen op [php]create_function[/php], hoe ranzig dat ook lijkt, makkelijker is. Dit komt uit een site waar ik nu mee aan het spelen ben (eentje zonder autoloading zoals je ziet ;) )
<?php
$container->add('pdo', create_function('', '
$pdo = new PDO("...", "...", "...");
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
return $pdo;'));
$container->add('db', create_function('$container', '
require_once "lib/database.php";
return new Database($container->pdo);'));
$container->add('authentication', create_function('$container', '
require_once "lib/authentication.php";
return new Authentication($container->users);'));
$container->add('users', create_function('$container', '
require_once "lib/user.php";
return new User_Store($container->db);'));
?>
Situatie 1 (pdo wordt meegegeven aan 'database'):
$container = new Container();
$container->add('pdo', 'PDO');
$container->add('database', 'Database', $container->pdo);
Situatie 2 ($config wordt meegegeven aan 'database'):
$container = new Container();
$config = array($db_name, $db_pass, $db_host, $db_table);
$container->add('database', 'Database', $config);
Kun je als je tijd hebt overigens eens schieten op m'n htaccess bestand? Klopt dit (voor zover jij kunt zien?). Of staat er teveel of te weinig in? Of dingen die niet kloppen?
Options All -Indexes
AddDefaultCharset utf-8
<IfModule mod_rewrite.c>
RewriteEngine On
RewriteCond %{REQUEST_FILENAME} !-f [OR]
RewriteCond %{REQUEST_FILENAME} .(inc|ini|p?ht.*|php.*|txt)$
RewriteRule ^(.*)$ index.php?route=$1 [QSA,NC,L]
</IfModule>
<IfModule mod_dir.c>
DirectoryIndex index.php index.html index.htm
</IfModule>
<Files ~ "\.(inc|ini|p?ht.*|php.*|txt)$">
order allow,deny
deny from all
</Files>
<Limit COPY DELETE GET LOCK MOVE POST PUT UNLOCK>
order deny,allow
deny from all
</Limit>
Hier dezelfde code, maar nu met commentaar regels erbij:
Options All -Indexes // zorgt dat er geen directory kan worden opgevraagd
AddDefaultCharset utf-8 // default characterset op utf-8
<IfModule mod_rewrite.c> // kijk of de rewrite module werkt, zo ja ga verder
RewriteEngine On // zet de rewrite engine aan
RewriteCond %{REQUEST_FILENAME} !-f [OR] // verwijst de url niet naar een bestand...
RewriteCond %{REQUEST_FILENAME} .(inc|ini|p?ht.*|php.*|txt)$ // of verwijst de url naar een bestand dat eindigt op inc ini (p)ht* php* txt...
RewriteRule ^(.*)$ index.php?route=$1 [QSA,NC,L] // verwijs de url dan door naar index.php
</IfModule>
// van het onderstaande ben ik niet geheel zeker...
<Files ~ "\.(inc|ini|p?ht.*|php.*|txt)$"> // hou requests tegen die eindigen op .inc .ini .(p)ht* .php* of .txt. (dit lijkt overigens totaal niet te werken)
order allow,deny
deny from all
</Files>
<Limit COPY DELETE GET LOCK MOVE POST PUT UNLOCK> // zorg dat bepaalde acties niet kunnen worden uitgevoerd bij een niet-browser aanroep (weet niet of dit wel klopt)
order deny,allow
deny from all
</Limit>
Options All -Indexes
AddDefaultCharset utf-8
<IfModule mod_rewrite.c>
RewriteEngine On
RewriteCond %{REQUEST_FILENAME} !-f [OR]
RewriteCond %{REQUEST_FILENAME} .(inc|ini|p?ht.*|php.*|txt)$
RewriteRule ^(.*)$ index.php?route=$1 [QSA,NC,L]
</IfModule>
<IfModule mod_dir.c>
DirectoryIndex index.php index.html index.htm
</IfModule>
<Files ~ "\.(inc|ini|p?ht.*|php.*|txt)$">
order allow,deny
deny from all
</Files>
<Limit COPY DELETE GET LOCK MOVE POST PUT UNLOCK>
order deny,allow
deny from all
</Limit>
Op het eerste gezicht:
- de "All" bij Options zou ik weglaten.
- Tweede RewriteCond is overbodig, zoals al eerder gezegd. Je zet immers alleen toegankelijke bestanden in de webroot, al het overige gaat erbuiten.
- Om bovenstaande reden is <Files> ook niet nodig.
- Je zou eventueel een RewriteRule kunnen toevoegen voor media bestanden, zodat deze niet via het framework gaan indien ze een 404 geven.
- De <Limit> is niet nodig, sowieso vraag ik me af of het werkt. Het lijkt nu alsof je bijvoorbeeld GET en POST niet toestaat. Dat lijkt me niet de bedoeling. Bovendien kan het geen kwaad als er een DELETE request wordt gedaan, zolang je deze niet ondersteunt. Je zou eventueel een RewriteRule kunnen toevoegen voor die alle requests met niet-geïmplementeerde methoden een 405 (Method not allowed) teruggeeft.
Op het eerste gezicht:
- de "All" bij Options zou ik weglaten.
[color="red"]Ik dacht ergens te lezen dat die All voor alle onderliggende directories geldt ofzo... maar weghalen dan maar?[/color]
- Tweede RewriteCond is overbodig, zoals al eerder gezegd. Je zet immers alleen toegankelijke bestanden in de webroot, al het overige gaat erbuiten.
[color="red"]Oke, maar ook htaccess wordt hierdoor beveiligd. Kan het kwaad om 'm te laten staan? Mocht er toch ooit per ongeluk een kritisch bestand in komen te staan?[/color]
- Om bovenstaande reden is <Files> ook niet nodig.
[color="red"]Oke, maar stel nu dat op een server de rewrite engine niet werkt, dan is dit toch een extra beveiliging? (als ik overigens de rewrite rules weghaal kan ik nog gewoon webbestanden aanroepen, dus dat hele FILES lijkt niks te doen. Of doe ik iets verkeerd?)[/color]
- Je zou eventueel een RewriteRule kunnen toevoegen voor media bestanden, zodat deze niet via het framework gaan indien ze een 404 geven.
[color="red"]Wat bedoel je precies? Media bestanden gaan nu toch sowieso niet via het framework?[/color]
- De <Limit> is niet nodig, sowieso vraag ik me af of het werkt. Het lijkt nu alsof je bijvoorbeeld GET en POST niet toestaat. Dat lijkt me niet de bedoeling. Bovendien kan het geen kwaad als er een DELETE request wordt gedaan, zolang je deze niet ondersteund. Je zou eventueel een RewriteRule kunnen toevoegen voor die alle requests met niet-geïmplementeerde methoden een 405 (Method not allowed) teruggeeft.
[color="red"]Ik dacht dat die Limit geldt voor aanroepen die niet via de browser worden gedaan (maar bijvoorbeeld via dos prompt / server) en dat dit voorkomt dat iemand via niet-browser aanroepen dingen doet die niet mogen. Maar zou goed kunnen dat hier helemaal niks van klopt wat ik nu zeg :)[/color]
Ik dacht ergens te lezen dat die All voor alle onderliggende directories geldt ofzo... maar weghalen dan maar?
Klik. Het kan niet heel veel kwaad als je All laat staan, persoonlijk gebruik ik het nooit.
Ozzie PHP op 06/01/2011 11:53:35
Oke, maar ook htaccess wordt hierdoor beveiligd. Kan het kwaad om 'm te laten staan? Mocht er toch ooit per ongeluk een kritisch bestand in komen te staan?
Ik ben nog nooit tegengekomen dat je door middel van http://www.example.com/.htaccess de .htaccess in kon zien. Wat dat betreft is het overbodig.
Ozzie PHP op 06/01/2011 11:53:35
Oke, maar stel nu dat op een server de rewrite engine niet werkt, dan is dit toch een extra beveiliging? (als ik overigens de rewrite rules weghaal kan ik nog gewoon webbestanden aanroepen, dus dat hele FILES lijkt niks te doen. Of doe ik iets verkeerd?)
Dan werkt je hele framework toch niet meer. Bovendien heb je nog steeds geen beschermde bestanden in de webroot staan, dus overbodig blijft het.
Ozzie PHP op 06/01/2011 11:53:35
Wat bedoel je precies? Media bestanden gaan nu toch sowieso niet via het framework?
Normaal gesproken (als een media bestand bestaat) niet. Maar stel nu dat je linkt naar een niet bestaand plaatje (bv. images/logo.jpg). De RewriteCond is dan waar, dus wordt je plaatje geroute naar http://www.example.com/index.php?route=images/logo.jpg. Oftewel via je framework. Dit is zonde van je performance, je kan dan beter meteen via .htaccess een 404 terug geven.
Ozzie PHP op 06/01/2011 11:53:35
Ik dacht dat die Limit geldt voor aanroepen die niet via de browser worden gedaan (maar bijvoorbeeld via dos prompt / server) en dat dit voorkomt dat iemand via niet-browser aanroepen dingen doet die niet mogen. Maar zou goed kunnen dat hier helemaal niks van klopt wat ik nu zeg :)
Klik. Dit klopt inderdaad niet :-) Wil je methoden blokken, dit werkt:
# Allow GET, HEAD and POST
RewriteCond %{REQUEST_METHOD} !^(GET|HEAD|POST)$ [NC]
RewriteRule .* - [F]
Let er wel op dat dit een 403 (Forbidden) teruggeeft. Dit zou eigenlijk een 405 moeten zijn, maar dan ben je verplicht om een Allow header mee te sturen. Bovenstaande is dus eigenlijk een quick-and-dirty fix.