Goedenavond allen,

Al langere tijd vind ik het leuk om wat met OOP programmeren te proberen (en vooral het idee van OO). Om die reden heb ik recent een MVC-Framework geschreven.
Dit heb ik nu in gebruik in een andere applicatie en ik loop tegen wat problemen aan.
Ik vind het moeilijk/onduidelijk de verantwoordelijkheden van de View en de controller te scheiden.

Voorbeeldcase:
Er wordt een request gedaan, deze wordt door de froncontroller opgevangen en de router/dispatcher zorgt ervoor dat de goede controller wordt aangeroepen en de bijbehorende method. In dit voorbeeld betreft het een method om een gastenboek bericht toe te voegen.
Je begint met het controleren van de meegegeven POST-waardes. Deze zijn echter niet correct omdat de gebruiker vergeten is zijn naam in te vullen.
Het bericht wordt dus niet toegevoegd en er moet worden verteld dat je de naam in moet vullen omdat het een verplicht formulierveld is en daar onder het formulier laat zien.
De schematische code

<?php
//voorbeeld code, de naam message is natuurlijk niet heel veel zeggend

public function addMessage()
{

    try
    {
        $oForm = new Form('index.php?c=Gastenboek&amp;m=addMessage', 'nameofform');
        $oNameField = new Text('name', new Label('Name:', 'class_no_error', 'class_error'));
        $oNameField->addValidator(new Required, new NotEmpty);
        $oMessageField = new Textarea('message', new Label('Your message:', 20, 10 'class_no_error', 'class_error_textarea'));
        $oMessageField->addValidator(new Required, new NotEmpty, new MinLength(3));
        $oForm->addElement($oNameField, $oMessageField);
    }
    catch(Exception $oException)
    {
        if(Framework::$DEBUG == true)
        {
            echo $oException->getMessage();
        }
        else
        {
            $this->setView(new Error('Formulier kon niet geladen worden'), abstractView::$Notoverridable);
        }
    }
    
    if($oForm->isSubmitted() && $oForm->isValid())
    {
        $oMessage = new Message($oNameField->getValue(), $oMessageField->getValue());
        $this->saveMessage($oMessage);
        header('Location: index.php?c=Gastenboek&m=Done');
    }
    elseif($oForm->isSubmitted() && !$oForm->isValid())
    {
        $oView = new addMessageView($oForm);
        $oErrorLog = $oForm->getErrorLog();
        foreach($oErrorLog as $oError)
        {
            if($oError->getValidator instanceof NotEmpty)
            {
                $oView->addError('Je ziet toch wel dat je een naam in moet vullen eikel!');
            }
            // etc elseif($oError->getValidator instanceof MinLength){ }
            
        }       
 
    }
    else
    {
        $oView = new addMessageView($oForm);
    }
    
    $this->setView($oView);
}


Alleen zoals deze voorbeeldcode nu is ben ik niet blij met de scheiding output/logica(/data):
Je zou willen dat als je iets aan de output van je applicatie wil veranderen dat je dan alleen de view in moet duiken, maar zoals het nu is geeft de controller de hardcoded data aan de view wanneer er wat fout gaat en moet je dus toch op meerdere plaatsen zoeken als je de output wilt veranderen.

Hoe lossen jullie dat op? Voor elke mogelijke foutmelding een view maken en die dan aan een andere view meegeven met een setError(Errorview $oView) functie? Dat lijkt me redelijk omslachtig omdat je 10000e foutmeldingen kunt hebben.

De kern van mijn probleem is dus hoe je hardcode data opslaat: in de view?(bijvoorbeeld een array met errornummers en errormeldingen en dan een setErrorNumber() functie?), in de controller?, in een Model?

Alvast bedankt,
Freek

[quote]
Edit
Veel typo's, leestekens
[quote]
<?php

interface DatabaseProvider {
public function getDatabase();
}

interface CacheProvider {
public function getCache();
}

abstract Class Context {

}

Class ControllerContext extends Context implements DatabaseProvider, CacheProvider {

public function getDatabase() {}
public function getCache() {}

}

Class ViewContext extends Context implements CacheProvider {

public function getCache() {}

}

class Controller {

public function __construct(DatabaseProvider $context) {}

// of

public function __construct(Context $context)
{
if(!$context instanceof DatabaseProvider)
throw new InvalidArgumentException('Wrong context');

// doe iets met de db

if(!$context instanceof CacheProvider)
throw new CompleteFailure('Still wrong context...');
}
}

$x = new ViewContext();
$y = new Controller($x); // prrr! fout
?>

Het maakt op zich niet zoveel uit of je interfaces gebruikt of gewoon een method aan Context toevoegt die je kan vragen of de context een bepaalde instantie van iets kan leveren. Eigenlijk is het puur syntactic sugar. Nu kan je aan de buitenkant van het object zien of het het goeie object is, en anders moet je het vragen. (In talen als C++ had het wel uitgemaakt omdat het daar tijdens compilen berekend kan worden. PHP doet de controle altijd pas wanneer hij hem tegenkomt bij het uitvoeren)


Ik gebruik dit ook om m'n model in op te slaan, omdat m'n config scriptje zeg maar hier op neerkomt:
<?php

$context = new Context();

$pdo = new PDO('...');
$context->set_pdo($pdo);

// if memcache beschikbaar
$cache = new Cache_Memcache('...');
$context->set_cache($cache);

$topic_store = new Topic_Store($pdo, $cache);
$context->set_topics($topic_store);

// userstore gebruikt andere cache, niet beschikbaar voor de controllers
$slave_cache = new Cache_Memcache('...');
$user_store = new User_Store($pdo, $slave_cache);
$context->set_users($user_store);

return $context;
?>

Maar je kan je context class ook zo schrijven dat hij pas objecten aanmaakt wanneer ze voor het eerst aangeroepen worden. Scheelt weer in opstarttijd van je script.

edit: maar soms wil je ook wel eens alle topics van een user hebben. Ik heb dat zo opgelost (door ze los te koppelen. Je kan beiden zonder de ander gebruiken in principe) (werkt wel alleen met 5.3 en wat trucjes)
<?php
User_Record::prototype()->topics = function(User_Record $user) use ($topic_store)
{
return $topic_store->find_topics_by_user($user);
};
?>
Trucje met getters, closures en late static binding in PHP 5.3. Ik voeg tijdens runtime methods aan User objecten toe om gemakkelijk bij hun topics te komen. Maar ik kan ook nog $topic_store->find_by_user($User) aanroepen. Ook weer syntactic sugar, maar ik vind $user->topics() toch leuker staan. Dit gaat echter wel weer compleet in tegen dat wat ik hierboven schreef over aan de buitenkant kunnen zien of een class iets kan. User_Record::topics() bestaat namelijk helemaal niet echt :)
Interessante materie.

Op deze manier de zaken aanpakken zit volgens mij wel muziek in. Enkel als je contexten wilt doorgeven bijvoorbeeld van de Controller naar de View wordt het wel een vager verhaal.

De view moet beschikken over bijvoorbeeld de language context, terwijl dit bij de controller niet in de context zit. Een oplossing zou zijn de view al in de frontController te setten, maar dat lijkt me niet de oplossing. En dan nog, dit probleem geldt ongetwijfeld niet alleen voor dit voorbeeld.

Verder wordt het tijd dat PHP5.3 uitkomt (sinds 11/6 RC3), namespaces bieden echt een groot voordeel (ten opzichte van o.a. de Poor Man's Namespace). Verder vind ik het dynamisch aanmaken van methoden bij objecten er veelbelovend uitzien.

Reageren