Search code examples
phpoopdesign-patternsrenderingencapsulation

Rendering without breaking encapsulation


What is the proper way to implement this without breaking encapsulation? I kept the example simple but the solution should scale well if I should decide to add more entities. I have a student and courses like so:

class Student {

    private $id;
    private $name;

    /**
     * Courses student is attending
     * @var Course[]
     */
    private $courses = array();
    private $numberOfAttendingCourses;

}

class Course {

    private $code;

}

Let's say I'd like to render this:

<ul>
    <li>[studentName] ([numberOfAttendingCourses])</li>
</ul>

and also this is the view in another use case

<ul>
    <li>[studentName] (ID: [studentId])
        <ul>
            <li>[course1Name] ([course1Code])</li>
            <li>[course2Name] ([course2Code])</li>
        </ul>
    </li>
</ul>

I know how to implementing this with getters and setters. I'm interested in the way which keeps both objects encapsulated as much as possible.


Solution

  • The easiest way would be to pass off value structure to your template renderer. This will keep you from being tempted to doing too much logic within the template itself.

    For example - First off create an interface for your renderable classes

    class Renderable {
         public function getViewData();
    }
    

    Then make your student and Course classes conform to the interface .

    class Student implements Renderable{
    
        private $id;
        private $name;
    
        /**
         * Courses student is attending
         * @var Course[]
         */
        private $courses = array();
        private $numberOfAttendingCourses;
        public function getViewData(){
            $dat = array("id"=>$this->id, "name"=>$this->name, "courses"=>array());
            foreach($this->courses as $course){
                $dat["courses"][] = $course->getViewData();
            }
            $dat["numCourses"] = count($dat["courses"]);
            return $dat;
        }
    
    }
    
    class Course implements Renderable{
    
        private $code;
        private $name;
    
        public function fetchViewData(){
            return array("code"=>$this->code,
                         "name"=>$this->name);
        }
    }
    

    The create a template parser.

    class Template {
         public function render($templateFile, array $data){
              if ($data instanceof Renderable){
                   $data = $data->getViewData();
              }
              extract ($data);
              ob_start();
              include($templateFile);
              ob_get_clean();
         }
    }
    

    Then a simple template for your markup

    <ul>
        <li><?= htmlspecialchars($name) ?> (ID: <?= htmlspecialchars($id) ?>)
            <ul>
                <?php foreach($courses as $course): ?>
                <li><?= htmlspecialchars($course["code"]) ?> (<?= htmlspecialchars($course["code"]) ?>)</li>
                <?php endforeach; ?>
            </ul>
        </li>
    </ul>
    

    Then once you have a valid Student object you can simply

    $template = new Template();
    $template->render("/path/to/template.phtml", $student->fetchViewData());
    

    This should also make it very simple for you to develop and test your templates as you would be able to use a serviceable array instead of going and creating a Student and a bunch of Course objects.