Door
Mich
op 18-10-2013 08:51
gewijzigd op 18-10-2013 10:23
4.310 views
Hoi,
Ik probeer de overgang van PHP Procedural naar PHP OOP te maken.
Om te begrijpen hoe OOP werkt en op welke manier het nuttiger kan zijn dan procedural wil ik graag jullie mening, opmerkingen, kritiek, etc horen over mijn eerste OOP script.
<?php
function printr($a){
echo '<pre>';print_r($a);echo'</pre>';
}
class Form {
public function __construct($fName){
$this->fName = '<form method="POST" name="' . $fName . '">'.PHP_EOL;
}
public function getForm(){
return $this->fName;
}
public function endForm(){
return '</form>'.PHP_EOL;
}
}
class Input {
public function __construct($iName){
$this->iName = $iName;
}
public function addType($iType){
$this->allowedTypes = Array('text','password','date','email','submit','checkbox','radio');
if(in_array($iType,$this->allowedTypes))
{
$this->iType = $iType;
}
else
{
throw new Exception('Input Type not allowed');
}
}
public function checked($iChecked){
if(($this->iType == 'checkbox' || $this->iType == 'radio') && $iChecked == TRUE)
{
$this->iChecked = TRUE;
}
else
{
$this->iChecked = FALSE;
}
}
public function group($iGroup){
if($iGroup == TRUE)
{
$this->iGroup = TRUE;
}
}
public function addValue($iValue){
$this->iValue = $iValue;
}
public function addPlaceholder($iPlaceholder){
$this->iPlaceholder = $iPlaceholder;
}
public function getInput(){
$input = '<input';
if(isset($this->iType))
{
$input .= ' type="' . $this->iType . '"';
}
if(isset($this->iName))
{
if(isset($this->iType) && ($this->iType == 'checkbox' || $this->iType == 'radio'))
{
$input .= ' name="' . $this->iName. '[]"';
}
else
{
$input .= ' name="' . $this->iName . '"';
}
$input .= ' id="' . $this->iName . '"';
}
if(isset($this->iValue))
{
$input .= ' value="' . $this->iValue . '"';
}
if(isset($this->iPlaceholder))
{
$input .= ' placeholder="' . $this->iPlaceholder .'"';
}
if(isset($this->iChecked) && $this->iChecked == TRUE)
{
$input .= ' checked="checked"';
}
$input .= ' /><br />'.PHP_EOL;
return $input;
}
}
class Select {
public function __construct($sName){
$this->sName = $sName;
}
public function addOption($sOptionValue,$sOption){
$this->sOption[][$sOptionValue] = $sOption;
}
public function getSelect(){
$selectbox = '<select name="' . $this->sName . '">'.PHP_EOL;
foreach($this->sOption AS $optionArray)
{
foreach($optionArray AS $value=>$option)
{
$selectbox .= '<option value="' . $value . '">' . $option . '</option>'.PHP_EOL;
}
}
$selectbox .= '</select><br />'.PHP_EOL;
return $selectbox;
}
}
class Box {
public function __construct($bName){
$this->bName = $bName;
}
public function addValue($bValue){
$this->bValue = $bValue;
}
public function addPlaceholder($bPlaceholder){
$this->bPlaceholder = $bPlaceholder;
}
public function getBox(){
$box = '<textarea name="' . $this->bName . '"';
if(isset($this->bPlaceholder)) { $box .= ' placeholder="' . $this->bPlaceholder . '"'; }
$box .= '>';
if(isset($this->bValue)) { $box .= $this->bValue; }
$box .= '</textarea><br />'.PHP_EOL;
return $box;
}
}
class Button {
public function __construct($bValue){
$this->bValue = $bValue;
}
public function getButton(){
return '<button>' . $this->bValue . '</button><br />'.PHP_EOL;
}
}
if(strtolower($_SERVER['REQUEST_METHOD']) == 'post')
{
printr($_POST);
}
$form = new Form('test');
echo $form->getForm();
$select = new Select('aanhef');
$select->addOption('','Maak een keuze');
$select->addOption('Meneer','Mr.');
$select->addOption('De heer','Dhr.');
$select->addOption('Mevrouw','Mw.');
$select->addOption('Dokter','Dr.');
$select->addOption('Anders','Anders');
echo $select->getSelect();
$input = new Input('name');
$input->addType('text');
$input->addValue('Jan');
$input->addPlaceholder('Naam van persoon');
echo $input->getInput();
$input = new Input('age');
$input->addType('text');
$input->addValue('36');
$input->addPlaceholder('Leeftijd van persoon');
echo $input->getInput();
$input = new Input('pass');
$input->addType('password');
$input->addValue('secret');
$input->addPlaceholder('Wachtwoord');
echo $input->getInput();
$input = new Input('optie');
$input->addType('checkbox');
$input->addValue('check optie 1');
$input->checked(TRUE);
echo $input->getInput();
$input = new Input('optie');
$input->addType('checkbox');
$input->addValue('check optie 2');
$input->checked(FALSE);
echo $input->getInput();
$input = new Input('optie');
$input->addType('radio');
$input->addValue('radio optie 1');
$input->checked(TRUE);
echo $input->getInput();
$input = new Input('optie');
$input->addType('radio');
$input->addValue('radio optie 2');
$input->checked(FALSE);
echo $input->getInput();
$box = new Box('message');
$box->addValue('');
$box->addPlaceholder('Typ hier je bericht');
echo $box->getBox();
$button = new Button('Opslaan');
echo $button->getButton();
echo $form->endForm();
?>
<?php
class Form {
public function __construct($fName){
$this->fName = '<form method="POST" name="' . $fName . '">'.PHP_EOL;
}
public function getForm(){
return $this->fName;
}
public function endForm(){
return '</form>'.PHP_EOL;
}
}
?>
Hartstikke mooi hoor zo een class en het ziet er verzorgd uit maar toch voor mij een cijfer vier. De reden? er is werkelijk niets flexibel aan deze class behalve de naam die je in de constructor meegeeft en zelfs die kun je later niet meer wijzigen.
De method kan ik niet opgeven of wijzigen en de action wordt al helemaal niet over gesproken.
Het renderen van de html doe je in mijn opzicht wanneer er om gevraagd wordt en niet in de constructor.
Ook zou ik de functies getForm wijzigen in startForm om de simpele reden een get.. functie daar verwacht ik een variabele uit terug en geen html.
Verder als dit heb ik het niet doorgelezen. genoeg Feedback denk ik.
Het begin is niet slecht, maar ik moet wel zeggen dat ik nog enigszins de echte 'OOP spirit' mis. Wat voor mij het belangrijke idee achter OOP is, is dat je een collectie aan classes krijgt die ieder hun eigen ding kunnen doen. Daarbij zijn ze zelfstandig en elke class zal dus zelf ervoor moeten zorgen dat de juiste output kan worden gegenereerd, zonder dat een andere class kennis nodig heeft van hoe iets gedaan wordt.
Als ik nu naar je form class kijk dan zie ik dat je twee methodes aanmaakt om het form te genereren 'getForm()' en 'endForm()'. Dat betekent dus dat de aanroepende class moet weten dat er een begin en eind is en dat de aanroepende class moet weten wanneer beide moeten worden aangeroepen. Dat vergt dus kennis in die aanroepende class die eigenlijk in je form class zou moeten. Vergeet de aanroepende class de tweede methode aan te roepen dan krijg je foutieve html, iets dat je nu juist simpel kan voorkomen als je in OOP werkt.
Als je kijkt naar html elementen dan zie in grove lijnen altijd de volgende structuur:
<opening tag - attributen>
content
<sluit tag>
De attributen, content en sluit tag zijn optioneel, die kunnen dus leeg zijn.
Deze structuur kan je overnemen in je html classes door een soort boom te bouwen, waarbij je elke html tag als een node ziet die 1 ouder heeft en eventueel meerdere kinderen. Als je dan de html code wilt produceren shrijf je eerst de opening tag weg, dan de attributen, dan de content (de kinderen) en uiteindelijk de sluit tag. Door middel van een heel simpele interface kan je afdwingen dat elke html class in staat is om zich als een kind en als een ouder te gedragen in de boom.
<?php
interface Html_Tag_Interface{
public function addChild( Html_Tag_Interface $child );
public function writeTag();
}
class Form_Class implements Html_Tag_Interface{
private $children = array();
public function addChild( Html_Tag_Interface $child ) {
$this->children[] = $child;
}
public function writeTag() {
$str = '<form>';
foreach( $this->children as $child ){
$str .= $child->writeTag();
}
$str .= '</form>';
return $str;
}
}
class Input_Class implements Html_Tag_Interface{
public function addChild( Html_Tag_Interface $child ) {
//inputs hebben geen kids, geen actie dus
}
public function writeTag() {
return '<input>';
}
}
//je form bouwen en schrijven kan nu als volgt:
$form = new Form_Class();
$form->addChild( new Input_Class() );
echo $form->writeTag();
?>
In deze structuur kan je alle html tags hangen en je kan dus de hele boom (beginnende bij de html root tag zelf) met 1 echo naar het scherm schrijven.
Wat nog wel ontbreekt zijn de attributen. Als ik in beide scripts kijk (van TS en van Donny) dan zie ik hardcoded attributen. Dit is dus niet echt OOP, want het is niet flexibel. Ik kan in beide bijvoorbeeld geen class meegeven aan het form. Laat staan custom attributen.
Ten eerste wil je dus flexibeler zijn, ten tweede moet je je ook bedenken dat alle html tags attributen kunnen hebben (op een paar uitzonderingen na) en dat als je de attributen afhandeling dus in de classes zelf zet, dat je dan continu hetzelfde aan het doen bent. Dit schreeuwt dus om zijn eigen class:
<?php
class Attributes_Class{
private $attributes = array();
public function setAttribute( $key, $value ){
$this->attributes[$key] = $value;
}
public function writeAttributes(){
$str = '';
foreach( $this->attributes as $key => $value ){
$str .= ' '.$key.'="'.$value.'"';
}
return $str;
}
}
//nieuw input class
class Input_Class implements Html_Tag_Interface{
private $attributes;
public function __construct( $type, $id, $name ){
$this->attributes = new Attributes_Class();
$this->attributes->setAttribute( 'type', $type );
$this->attributes->setAttribute( 'id', $id );
$this->attributes->setAttribute( 'name', $name );
}
public function addChild( Html_Tag_Interface $child ) {
//inputs hebben geen kids, geen actie dus
}
public function writeTag() {
return '<input'.$this->attributes->writeAttributes().'>';
}
}
//form met 1 text input:
$form = new Form_Class();
$form->addChild( new Input_Class( 'text', 'edUsername', 'username' ) );
echo $form->writeTag();
?>
Uiteraard is dit een zeer eenvoudig implementatie, maar met deze structuur kan je het zo ver uitbouwen als je wilt.
Verder nog een paar kleine opmerkingen die wellicht kunnen helpen:
- ik zie in beide scripts dat er ofwel een PHP_EOL, ofwel een break tag achter de html gegeven wordt. Enerzijds is dat niet nodig, anderzijds ook niet gewensd. Het is niet aan een simpele class om te bepalen wat er aan output komt na de eigen output. Een class doet wat hij moet doen, maar je wil niet dat een class over zijn graf regeert en dus gaat bepalen wat er buiten de class gebeurt.
- ik zie ook dat je variabelen in je class gebruikt die je niet hebt gedefinieerd. Dat kan, maar is niet echt netjes. Dit maakt het zeer lastig om op een later moment nog te kunnen zien welke variabelen beschikbaar zijn in de class.
Ik reageer even algemeen op iedereen. Heel erg bedankt voor de goede en duidelijke feedback! Het klopt inderdaad dat het niet flexibel is en veel attributen dubbel zijn. Zelf vond ik dat 'endform' ook niet handig, maar wist dit nog niet anders te doen.
Ik ga met deze feedback aan de slag en zal later nog eens wat posten.
Toevoeging op 21/10/2013 10:40:00:
Zou iemand mij in jip en janneke taal kunnen uitleggen wat 'interface' nou doet?
Ik lees dat dit lijkt op abstract die een functie verplicht maakt, maar dit is toch niet wat interface doet? Of begrijp ik abstract nou ook verkeerd.
Terug komend op de reactie van Erwin heb ik nou het volgende script waarin ik de Interface (nog) niet gebruik. Wat is nou de meerwaarde van 'interface' zoals in het script van Erwin, terwijl het script hieronder vrijwel het zelfde is?
Wat is nou de 'goede' manier om je output te 'stylen'? Ik begrijp van de berichten hierboven dat je niet teveel HTML in je class/function wilt, want dan is het niet meer flexibel. Maar nou wil ik bijv achter de input text een break, maar achter een checkbox of radio weer niet. Doe ik dit dan in de class/function of bij de output?
<?php
interface Html_Tag_Interface{
}
class Form{
private $children = Array();
private $formAttributes = Array();
function __construct($method = null, $action = null){
if(isset($method))
{
if(strtolower($method) == 'post' || strtolower($method) == 'get')
{
$this->formAttributes['method'] = $method;
}
else
{
throw new Exception('Method not allowed');
}
}
if(isset($action)){
$this->formAttributes['action'] = $action;
}
}
public function addChild($child){
$this->children[] = $child;
}
public function writeTag(){
$str = '<form';
if(isset($this->formAttributes))
{
foreach($this->formAttributes AS $key=>$value)
{
$str .= ' '. $key .'="'. $value .'"';
}
}
$str .= '>';
foreach($this->children AS $child)
{
$str .= $child->writeTag();
}
$str .= '</form>';
return $str;
}
}
class Attributes{
private $attributes = Array();
public function setAttribute($key,$value){
$this->attributes[$key] = $value;
}
public function writeAttributes(){
$str = '';
foreach( $this->attributes AS $key=>$value )
{
$str .= ' '. $key .'="'. $value.'"';
}
return $str;
}
}
class Input{
private $attributes;
private $allowedTypes = Array('text','password','submit','checkbox','radio','color','date','datetime','email','month','number','range','search','tel','time','url','week');
public function __construct( $type, $id, $name ){
if(!in_array(strtolower($type),$this->allowedTypes))
{
throw new Exception('Input Type not allowed');
}
$this->attributes = new Attributes;
$this->attributes->setAttribute('type',$type);
$this->attributes->setAttribute('id',$id);
$this->attributes->setAttribute('name',$name);
}
public function writeTag() {
return '<input'.$this->attributes->writeAttributes().' />';
}
}
$form = new Form('post');
$form->addChild( new Input( 'text' , 'test' , 'blabla' ) );
$form->addChild( new Input( 'submit' , 'test' , 'blabla' ) );
echo $form->writeTag();
?>
Een interface kan je zien als een contract tussen jou en je programma. je interface zegt je moet deze methoden gebruiken en hoe jij die methodes invult dat zoek je zelf maar uit
vb ( code is gewoon van php.net geplukt )
<?php
interface iTemplate
{
public function setVariable($name, $var);
public function getHtml($template);
}
?>
Je code zou dan ingevuld kunnen worden in een klasse die je aanmaakt waarvan deze de iTemplate implementeert
<?php
class Template implements iTemplate
{
private $vars = array();
public function setVariable($name, $var)
{
$this->vars[$name] = $var;
}
public function getHtml($template)
{
foreach($this->vars as $name => $value) {
$template = str_replace('{' . $name . '}', $value, $template);
}
return $template;
}
}
?>
Je programmeert hierbij naar een interface toe en niet naar een implementatie ( wat dus goed is en aanbevolen. )
Wil je nu bijvoorbeeld ook methoden verplicht maken maar wil je een methode voor elke klasse hetzelfde hebben dan maak je een abstracte klasse
vb ( van php.net )
<?php
abstract class AbstractClass
{
// Force Extending class to define this method
abstract protected function getValue();
abstract protected function prefixValue($prefix);
// Common method
public function printOut() {
print $this->getValue() . "\n";
}
}
?>
Zo zie je dat een abstracte klasse de methodes getValue() en prefixValue() als abstract declareerd en de printOut(); functie niet. tevens krijgt deze methode ook logica in zich.
dat wil zeggen als jij een klasse aanmaakt.
<?php
class ConcreteClass1 extends AbstractClass
{
protected function getValue() {
return "ConcreteClass1";
}
public function prefixValue($prefix) {
return "{$prefix}ConcreteClass1";
}
}
?>
zie je dat je extends doet ( overerving ) inheritance in het engels.
Nu heeft deze klasse de twee abstracte methodes overgeërfd. Wat je niet ziet in de klasse is de methode printOut(). Heel simpel omdat deze door de parentklasse al ingevuld is.
Verder is het verschil dat je meerdere interfaces kan implementeren en maar 1 klasse kan extenden.
Uh, nee, het is eerder het Composite pattern: http://en.wikipedia.org/wiki/Composite_pattern
Zeker geen strategy pattern, want in een strategy pattern laat je in feite een deel van de implementatie van een class afhangen van de implementatie van een ander (geinjecteerd) object. Dat is in dit geval niet aan de orde. Je zou met veel fantasie de attributen class een implementatie van het strategy pattern kunnen noemen, maar dat vind ik persoonlijk een beetje vergezocht. Die class heeft namelijk niet de intentie om uitgewisseld te kunnen worden (om te zorgen voor een andere implementatie), het is simpel bedoeld om algemene functionaliteit uit de html classes te halen.
Een interface is simpel gezegd een blauwdruk voor je class. De interface stelt van te voren vast wat een class moet kunnen. Hierdoor kan een andere class dus al weten wat bepaalde class doet en welke methodes in elk geval beschikbaar zijn.
Neem het voorbeeld van het form. De form class kan kinderen hebben die ook allemaal html moeten kunnen uitspugen. Maar wat voor html, hoe het eruitziet en hoe die kinderen dat doen maakt die form class geen moer uit. Als die de methode 'writeTag' maar kan aanroepen. Dat is in feite wat de interface afdwingt. Je ziet ook dat ik in de addChild methode als type hint de interface heb gezet. Dat betekent dat ik alleen een object mee kan geven van een class die die interface implementeert. Geef ik iets anders mee dan krijg ik een fatale foutmelding.
Met die structuur kan ik dus op elk moment een nieuwe class schrijven die de html interface implementeert. Mijn form class kan onveranderd blijven en toch al die nieuwe classes gebruiken als onderdeel van de html boom.
Wat betreft de extra breaks aan het einde van een object, dat moet je nooit willen doen. Het is namelijk niet aan een form of input object om te bepalen wat er na komt. Wat als je morgen toch een input wilt hebben in een text, zonder break? Dan kan je je class opeens niet meer gebruiken. Breaks zijn in feite gewoon hun eigen object. Eventueel kan je in je form class die breaks na een input wel zetten, omdat de form class wel kan bepalen wat er binnen de form tags komt, maar zelfs dan kan je nog in bovenstaande situatie komen.
Bedankt voor de uitleg! Ik ga mijn best doen om het allemaal te begrijpen.
Het is een hoop informatie die even de tijd nodig heeft.
Wat ik nog niet goed begrijp is het HTML gedeelte. Meerdere mensen vinden dat HTML niet in OOP hoort, maar in dit form-voorbeeld zou dat betekenen dat het grootste gedeelte buiten de OOP zou moeten plaats vinden en form misschien een slecht voorbeeld is.
Terugkomend op het script van Erwin heeft hij een functie writeTag() die alles in één keer output. Hierin kan ik dus buiten OOP geen HTML, zoals breaks, labels, aan toevoegen. Hoe lost men dit op?
Ik denk dat je HTML in OOP verwart met het niet direct echoen van HTML uit een functie. Uiteindelijk moet een php script ergens de HTML uitspugen dus je ontkomt er niet aan dat ergens de HTML opgebouwd moet worden. Uiteraard kan dat via includes (welke je ook vrij simpel in deze structuur in kan voegen), maar helemaal met includes lijkt me vrij lastig.
Wat veel mensen echter wel vinden is dat je vanuit een functie niet direct output naar de browser moet sturen en dat gebeurt hier ook niet. Geen van de classes echoen zelf, ze geven alleen maar de string terug die dan geechoed kan worden.
Wat betreft de OOP concepten (als interfaces, design patterns etc), maar je daar niet al te druk over. Dat kost tijd en je zal vast nog wel wat classes gaan schrijven voor je alles helemaal door hebt. Die tijd heeft iedereen nodig.
De writeTag methode is misschien wat misleidend. Die methode schrijft in principe zelf namelijk niets naar de browser en in feite zou ik dus geen 'write' moeten gebruiken in de naam. Daarmee zou je vraag ook al bijna beantwoord moeten zijn, want buiten de classes die in die writeTag methode de HTML opbouwen, kan je dus nog vanalles met die string doen, inclusief er iets aan hangen, bijvoorbeeld een break. Maar net als de labels die je noemt zijn dat ook gewoon HTML tags en ook die kan je dus gewoon als een object erin plakken:
<?php
class Label_Class implements Html_Tag_Interface{
private $attributes;
private $children;
public function __construct( $for ){
$this->attributes = new Attributes_Class();
$this->attributes->setAttribute( 'for', $for );
}
public function addChild( Html_Tag_Interface $child ) {
$this->children[] = $child;
}
public function writeTag() {
$str = '<label'.$this->attributes->writeAttributes().'>';
foreach( $children as $child ){
$str .= $child;
}
$str .= '</label>';
return $str;
}
}
$form = new Form_Class();
$label = new Label_Class( 'edUsername' );
$label->addChild( new Input_Class( 'text', 'edUsername', 'username' ) );
$form->addChild( $label );
echo $form->writeTag();
?>
Etcetera, etcetera.
Maar nu wel even een waarschuwing, ga uiteindelijk niet voor elke tag een eigen class schrijven. Hoewel het kan, zal het niet verstandig zijn. Dan ga je namelijk honderden classes per pagina gebruiken en uiteindelijk is dat niet bevorderlijk voor de performance. Bovenstaande is het concept, maar in praktijk bouw ik classes die een heel deel van een pagina opmaken. Bijvoorbeeld een form class die uiteindelijk het hele form kan bouwen, inclusief de labels en inputs, maar ook nog willekeurig welk ander object kan meenemen als kind. Door die addChild methode kan alles erin gevoegd worden. Hetzelfde voor tabellen. Daarvoor heb ik een object dat extra methodes heeft om de headers door te geven, kolommen, rijen etc. In writeTag() zal die class dan de hele tabel opbouwen en er zijn dus geen aparte classes voor de row en cell tags.
Uiteindelijk kom je dan grotendeels wel weer terug bij de class waar je het topic mee begon, maar hopelijk wel met een wat andere implementatie.