Using the Collection Class

Using the Collection Class

As we've already mentioned several times in this chapter, our registrar's office application involves a class called Student that contains a collection of Course objects. Both classes are fairly simple. The UML class diagram is shown in Figure 5-3.

Figure 5-3

Of course, a Course should also have a collection of students, but the code will be quite similar, so after you've seen how we implement the courses collection for the Student class, it we'll leave it as an exercise for you to update the UML diagram and add the necessary code to support the students collection on the Course class.

For the Collection class, we're using the code exactly as it appears in the example with the NightClub object previously, so if you've already created that class you don't need to make any changes. If you haven't already created a file containing that class, and you want to try the example out for yourself, do so now.

From the UML diagram (Figure 5-3) you can see that the course class is quite simple, having only a name, a course code, and an ID. The course code is the course identifier, such as CS101 for a beginning computer science class. The name is the full name of the course. The ID is the numeric primary key from the database. In a real application, you would probably want to have access to a list of prerequisites (another collection), a syllabus outline, information about the instructor, and so on. Again, these are left as an exercise for you to complete on your own.

Have a look at the code:

   <?php    class Course {      private $_id;      private $_courseCode;      private $_name;      function __construct($id, $courseCode, $name) {        $this->_id = $id;        $this->_courseCode = $courseCode;        $this->_name = $name;      }      public function getName() {        return $this->_name;      }      public function getID() {        return $this->_id;      }      public function getCourseCode() {        return $this->_courseCode;      }      public function __toString() {        return $this->_name;      }    }    ?> 

According to the class diagram, a course has two properties, $id and $name. Because this part of the application should not allow the name or ID of the class to be modified, we've used private member variables to store those values, and we provided getID() and getName() methods for retrieving them.When you build the parts of the application that do allow these values to be modified (the course administration tools), you can add setID() and setName() methods. These methods should probably utilize some sort of security mechanism to ensure that no unauthorized individuals are modifying properties of a course.

We've also provided a __toString() method. This "magic'' method can be defined by any objects in PHP5. It allows you to write code like this:

   print "This course is called " . $objCourse . "<br>"; 

$objCourse is obviously not a string, but because the special __toString() method has been defined, PHP will use the return value of that method when attempting to coerce the variable from an object to a string.

Because the courses collection should only ever consist of Course objects, create a subclass of Collection called CourseCollection. Override the addItem() method to include a type hint that will allow only Course objects to be added to the collection.

   <?php    class CourseCollection extends Collection {      public function addItem(Course $obj, $key = null) {        parent::addItem($obj, $key);      }    }    ?> 

This will allow PHP to do some type checking to ensure that everything in the $courses collection is exactly what you expect it to be a Course object.

The Student class has the properties $id and $name, which represent the unique identifier and name of that student. It also needs to have a property called $courses, which is a collection of the courses for which the student is currently registered.

   <?php    class Student {      private $_id;      private $_name;      public $courses;      public function __construct($id, $name) {        $this->_id = $id;        $this->_name = $name;        $this->courses = new CourseCollection();        $this->courses->setLoadCallback('_loadCourses', $this);      }      public function getName() {        return $this->_name;      }      public function getID() {        return $this->_id;      }      private function _loadCourses(Collection $col) {        $arCourses = StudentFactory::getCoursesForStudent($this->_id, $col);      }      public function __toString() {        return $this->_name;      }    }    ?> 

Just as in the Course class, implement the $id and $name properties as private member variables and provide getID() and getName() functions. For now, leave the $courses member as a public member variable. Note the creation of a __toString() method for this class, as well. This makes it a little bit easier to display the most common string representation of any object, in this case the name of the student.

In the constructor, call setLoadCallback() and pass in the name of the loading method _loadCourses() and a reference to the object where that method exists, namely, $this.

The _loadCourses() method takes a Collection as its only parameter, as required by the setLoadCallback() function, and uses a static method of the StudentFactory class (which we haven't discussed yet). This method requires the ID of the student and is passed a Collection object into which to load the courses.

The StudentFactory class contains two static methods that do all the work in the database of loading the student and course information. For this application, three tables need to be created in your database: student, course, and studentcourse. The purpose of the first two tables should be fairly obvious to store the basic information about the students and the courses. The third table has two fields, studentid and courseid. It creates the association between students and courses.

The SQL statements that follow are for PostgreSQL (our preferred database server), but with minor modifications it would also work for most RDBMSes.

   CREATE TABLE "student" (       "studentid" SERIAL NOT NULL PRIMARY KEY,       "name" varchar(255)    );    CREATE TABLE "course" (      "courseid" SERIAL NOT NULL PRIMARY KEY,      "coursecode" varchar(10),      "name" varchar(255)    );    CREATE TABLE "studentcourse" (       "studentid" integer,       "courseid" integer,       CONSTRAINT "fk_studentcourse_studentid"         FOREIGN KEY ("studentid")         REFERENCES "student"("studentid"),       CONSTRAINT "fk_studentcourse_courseid"         FOREIGN KEY ("courseid")         REFERENCES "course"("courseid")    );    CREATE UNIQUE INDEX "idx_studentcourse_unique"        ON "studentcourse"("studentid", "courseid"); 

Be sure that the foreign keys are created and don't overlook the unique constraint of the studentcourse table. A student should not be able to register twice for the same course, and this constraint enforces this fact at database-level.

Insert some sample data into each of the three tables. Statements like the following should work for the purposes of this exercise:

   INSERT INTO "student"(name) VALUES('Bob Smith');    -- studentid 1    INSERT INTO "student"(name) VALUES('John Doe');     -- studentid 2    INSERT INTO "student"(name) VALUES('Jane Baker');   -- studentid 3    INSERT INTO "course"("coursecode", "name")        VALUES('CS101', 'Intro to Computer Science');   -- courseid 1    INSERT INTO "course"("coursecode", "name")        VALUES('HIST369', 'British History 1945 1990'); -- courseid 2    INSERT INTO "course"("coursecode", "name")        VALUES('BIO546', 'Advanced Genetics');          -- courseid 3    INSERT INTO "studentcourse"("studentid", "courseid") VALUES(1, 1);    INSERT INTO "studentcourse"("studentid", "courseid") VALUES(1, 2);    INSERT INTO "studentcourse"("studentid", "courseid") VALUES(1, 3);    INSERT INTO "studentcourse"("studentid", "courseid") VALUES(2, 1);    INSERT INTO "studentcourse"("studentid", "courseid") VALUES(2, 3);    INSERT INTO "studentcourse"("studentid", "courseid") VALUES(3, 2); 

The last six statements should be adjusted depending on the studentid and courseid values that result from the insert statements into student and course.

With the database tables created and populated with sample data, you can write and test the StudentFactory class. This class has two static methods: getStudent() and getCoursesForStudent(). The former is responsible for creating a Student object given a student ID. The latter populates the courses collection for a given student. In the code shown next, we're not demonstrating the use of any particular database functions, so some pseudo code is standing in for the calls to actual database functions. In Chapter 8, "Database Abstraction Layers,'' we show you how to use the PEAR::DB database abstraction functions, but for now you should feel free to substitute the explicit calls to pg_connect, pg_query, and so on (or whatever the appropriate PHP functions are for your database type).

   <?php    class StudentFactory {      public static function getStudent($id) {        $sql = "SELECT * from \"student\" WHERE \"studentid\" = $id";        $data = $db->select($sql); //pseudo code. Assume it returns an                                   //array containing all rows returned                                   //by the query.        if(is_array($data) && sizeof($data)) {          return new Student($data[0]['studentid'], $data[0]['name']);        } else {          throw new Exception("Student $id does not exist.");        }      }      public static function getCoursesForStudent($id, $col) {        $sql = "SELECT \"course\".\"courseid\",                       \"course\".\"coursecode\",                       \"course\".\"name\"                FROM \"course\", \"studentcourse\" WHERE                       \"course\".\"id\"=                       \"studentcourse\".\"courseid\" AND                       \"studentcourse\".\"studentid\" = $id";        $data = $db->select($sql);  //same pseudo code in getStudent()        if(is_array($data) && sizeof($data)) {          foreach($data as $datum) {            $objCourse = new Course($datum['courseid'], $datum['coursecode'],                                    $datum['name']);            $col->addItem($objCourse, $objCourse->getCourseCode());          }        }      }    }    ?> 

The getStudent() method returns a new Student object populated with the appropriate data. If no student exists in the database with that ID, an exception is thrown. All calls to StudentFactory::getStudent() should be wrapped in a try...catch statement.

The getCoursesForStudent() method is passed the $courses collection from within _loadCourses(). The database query selects all the data from the course table, employing an implicit JOIN to the studentcourse table to get the courses associated with the specified student.

Based on the data returned from the database (if any), new Course objects are added to the collection. Note that we're using the coursecode value as the key when adding the Course to the courses collection.

Because all objects in PHP5 are passed by reference, we are able to alter the contents of the collection from within getCoursesForStudent() and see those changes manifest in the $courses variable of the Student object. If the phrase "passed by reference" doesn't mean much to you, see the discussion in Chapter 4, "Design Patterns,'' on the difference between function parameters passed by value versus those passed by reference.

To finally put all this good stuff to some use, try the following code:

   <?php    $studentID = 1; //use a valid studentid value from your student table    try {      $objStudent = StudentFactory::getStudent($studentID);    } catch (Exception e) {      die("Student #$studentID doesn't exist in the database!");    }    print $objStudent .          ($objStudent->courses->exists('CS101')?'is':'isnot').          'currently enrolled in CS101';    //displays: "Bob Smith is enrolled in CS101"    ?> 

In a deployed application, you should do something a bit more graceful than an abrupt termination of the application if the specified student isn't found, but we hope you get the idea of why that try...catch statement needs to be used.

After getting $objStudent, this code displays a simple message letting you know whether this student is enrolled in CS101. Upon invoking the exists() method, the courses collection is populated from the database, courtesy of the StudentFactory. Because the course code was used as the key when adding Course objects to the collection, you can use the course code ('CS101') as the parameter to exists().

Professional PHP5 (Programmer to Programmer Series)
Professional PHP5 (Programmer to Programmer Series)
Year: 2003
Pages: 182
BUY ON AMAZON © 2008-2017.
If you may any questions please contact us: