4
\$\begingroup\$

Thanks in advance for any insight. All used classes are at the top, and everything starts at the comment:

// where the magic happens

In particular, I am looking for feedback on my attempt at using the factory method and dependency injection. However, I would appreciate any other feedback as well. I have a list of various questions at the bottom.

<?php

class Config {

    // Class that holds config info.
    // Besides PDO connection info, contains flags for testing mode, live, eCommerce-enabled, etc.
    // no setters, only getters... I'm thinking of this class as a glorified associative array
    protected $properties = array();
    function __construct()
    {
        // reads app config file(s), sets various keys in $this->properties;
    }

    public function get($key)
    {
        return $this->properties[$key];
    }
}

class Request {

    protected $server;
    protected $get;
    protected $post;

    function __construct(array $server, array $get, array $post)
    {
        $this->server = $server;
        $this->get = $get;
        $this->post = $post;
    }

    // other methods in here like getUri, isAjax, 
    // getRequestMethod, getPost, getGet, getAgent, getRemoteAddr
}

class Session {

    function __construct()
    {
        // Start the session
        session_start();
        // Set a user ID cookie, etc
    }
    // other session-related setters and getters
}

class ModelFactory{

    protected $className; // class to instantiate (string)

    function __construct($className)
    {
        $this->className = $className;
    }

    public function build(Config $c, Request $r, Session $s)
    {
        $pdo = new PDO(
                $c->get('dsn') , 
                $c->get('pdo_user') , 
                $c->get('pdo_pass'),
                 $c->get('pdo_options')
            );
        return new $this->className($pdo, $c, $r, $s);
    }
}

class ControllerFactory{

    public function build()
    {
        $c = new Config();
        $r = new Request($_SERVER, $_GET, $_POST);
        $s = new Session();

        // Reads config file with route info, compares it to $r->getUri
            // to find name of controller and action within controller.
        $name = 'Controller_Example';
        $action = 'action_showComments';

        // Returns correct child object of Controller.
        return new $name($c, $r, $s, $action);
    }
}

abstract class Controller{

    protected $config;
    protected $request;
    protected $session;
    protected $action;

    function __construct(Config $c, Request $r, Session $s, $action )
    {
        $this->config = $c;
        $this->request = $r;
        $this->session = $s;
        $this->action = $action; // string of name of function to execute
    }
    protected function before() { /* some extendable code to execute before action */ }
    protected function after(){ /* some extendable code to execute after action */ }
    public function execute()
    {
        // would an output buffer be good here? eg. ob_start()
        // only doing this because im not sure if $this->$this->action() would work
            $method_to_execute = $this->action; 
        $this->before();
        $this->$method_to_execute();
        $this->after();
    }
}

class Controller_Example extends Controller{
    /**
     * A function that gets list of recent comments.
     */
    public function action_showComments()
    {
        $m_factory = new ModelFactory('Model_Comment');
        $comment_model = $m_factory->build($this->config, $this->request, $this->session);
        $comments = $comment_model->getComments();
        $title = 'Displaying Comments';
        $bid = 'comment';
        include '/path/to/views/commentview.php'; // see below for commentview.php
    }
}

class Model_Comment {

    protected $config;
    protected $request;
    protected $session;
    protected $pdo;

    function __construct(PDO $pdo, Config $c, Request $r, Session $s, )
    {
        $this->config = $c;
        $this->request = $c;
        $this->session = $c;
        $this->pdo = $pdo;
    }
    public function getComments()
    {
        // uses $this->pdo to query database, returns an array of comments
        // may or may not use request, session, or config objects
        return array('This is a comment', 'So is this', 'And this is too!');
    }

}

// where the magic happens
$factory = new ControllerFactory();
$controller = $factory->build();
$controller->execute();

?>

<!-- commentview.php -->
<!DOCTYPE html>
<head>
    <title><?php echo $title ?></title>
</head>
<body id="<?php echo $bid ?>">
    <ul>
        <?php
        foreach ($comments as $comment) {
            echo '<li>'.$comment.'</li>';
        }
        ?>
    </ul>
</body>
<!-- end commentview.php -->

Questions:

  1. Does $this->$this->action() work correctly if $this->action is a string which is the name of a method in the same class (see Controller::execute()) ?

  2. If not every model needs the config, request, and session objects, does ModelFactory::build() violate the law of demeter? How can I avoid this problem?

  3. As a corollary, is there a way I can make the parameters for Factory::build() variable in order to have an abstract class or interface for all factories? Example:

    abstract class Factory{ abstract function build({variable params}) }
    
  4. Won't having new operators for factories in places like Controller_Example::action_showComments() defeat the purpose of DI making code more testable?

  5. How could I approach templating html pages in this app?

  6. What are some advantages for using ob_start() in a situation like Controller::execute()?

\$\endgroup\$

2 Answers 2

1
\$\begingroup\$

1. $this->$this->action()

You could do this with $this->{$this->action}(). The braces are important for the precedence. PHP wants to break up $this->this->action() first into $this->$this (using the second $this as a string to get the property from the first $this) and then call the action method with that property.

2. LoD vs LSP

I don't think it is exactly the Law of Demeter that is broken. I think it might be the Liskov Substitution Principle.

3. Factory::build

Yes. There are three situations:

// No parameters
return new $className;

// One parameter
return new $className($param);

// Multiple parameters
$object = new \ReflectionClass($className);
return $object->newInstanceArgs($params);

4. Testing with new

See this for testing with new.

5. Templating

Personally I use plain PHP.

6. ob_start

The advantage of buffered output is that nothing is sent until you want it to be. This is important when you may want to set response headers (which must be the first output that is sent).

Config

// I'm thinking of this class as a glorified associative array

This is a great place to implement ArrayAccess. You can then access your config settings with:

$config['pdo_user'] // I prefer $config over $c

\$\endgroup\$
6
  • \$\begingroup\$ Really appreciate the response! Accepted for answering most of my questions (and the most pressing ones at that). Any other insight you have time for would be greatly appreciated as well. \$\endgroup\$ Mar 29, 2012 at 6:11
  • \$\begingroup\$ I've added 2, 5 and 6 now. I'm glad 1, 3, 4 was helpful. \$\endgroup\$
    – Paul
    Mar 30, 2012 at 14:00
  • \$\begingroup\$ awesome, this has helped me out a lot. Cheers! \$\endgroup\$ Mar 30, 2012 at 15:40
  • \$\begingroup\$ Quick question... what does the backslash in new \ReflectionClass do? \$\endgroup\$ Apr 2, 2012 at 16:02
  • \$\begingroup\$ That was just from copy & paste. It is used with namespaces to refer to the global scope. In code with no namespacing there should be no difference with or without it. \$\endgroup\$
    – Paul
    Apr 2, 2012 at 23:33
0
\$\begingroup\$

2. LoD, 3. variable params and 4. DI

I wouldn't create these model factories at all. Why? Bacause the model classes (I would name them Repositories) can be created automatically by Dependency Injection Container. Repositories can have really different dependecies: DB connection, memcache instance, etc. I don't think repositories should know about request or session (if it's not CommentSessionRepository).

So you can then just write something like this:

class Controller_Example extends Controller{

    private $repo;

    public function __construct(CommentRepository $repo)
    {
        $this->repo = $repo;
    }

    public function action_showComments()
    {
        $comments = $this->repo->getComments();
        $title = 'Displaying Comments';
        $bid = 'comment';
        include '/path/to/views/commentview.php'; // see below for commentview.php
   }
}

And one little note: It's always better when repositories return objects (entities) not arrays. You are then sure what do you work with.

5. templating

The best templating system I have ever seen in PHP is in Nette framework: http://nette.org

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

Not the answer you're looking for? Browse other questions tagged or ask your own question.