The basics of data and logic that you’ve seen so far are enough to get lots of things done in PHP. An additional concept—object-oriented programming, which combines data with the logic that operates on it—helps to organize your code. In particular, objects are great for making reusable bundles of code, so being familiar with how they work will make it easier to use lots of existing PHP add-ons and libraries.
In the programming world, an object is a structure that combines data about a thing (such as the ingredients in an entrée) with actions on that thing (such as determining if a certain ingredient is in the entrée). Using objects in a program provides an organizational structure for grouping related variables and functions together.
Here are some basic terms to know when working with objects:
Entree class would
contain variables that hold its name and ingredients. The functions
in an Entree class would be for things such as
cooking the entrée, serving it, and determining whether a particular
ingredient is in it.
Entree class. While each of these instances is
based on the same class, they differ internally by having different
property values. The methods in each instance contain the same
instructions, but probably produce different results because they
each rely on the particular property values in their instance.
Creating a new instance of a class is called
“instantiating an object.”Example 6-1 defines an Entree class to represent an entrée.
classEntree{public$name;public$ingredients=array();publicfunctionhasIngredient($ingredient){returnin_array($ingredient,$this->ingredients);}}
In Example 6-1, the class definition starts with the special keyword class followed by the name we’re giving to the class. After the class name, everything between the curly braces is the definition of the class—the properties and methods of the class. This class has two properties ($name and $ingredients) and one method (hasIngredient()). The public keyword tells the PHP engine which parts of your program are allowed to access the particular property or method the keyword is attached to. We’ll get into that later, in “Property and Method Visibility”.
The hasIngredient() method looks mostly like a regular function definition, but its body contains something new: $this. This is a special variable that refers to whatever instance of a class is calling the function. Example 6-2 shows this in action with two different instances.
// Create an instance and assign it to $soup$soup=newEntree;// Set $soup's properties$soup->name='Chicken Soup';$soup->ingredients=array('chicken','water');// Create a separate instance and assign it to $sandwich$sandwich=newEntree;// Set $sandwich's properties$sandwich->name='Chicken Sandwich';$sandwich->ingredients=array('chicken','bread');foreach(['chicken','lemon','bread','water']as$ing){if($soup->hasIngredient($ing)){"Soup contains$ing.\n";}if($sandwich->hasIngredient($ing)){"Sandwich contains$ing.\n";}}
The new operator returns a new Entree object, so in Example 6-2, $soup and $sandwich each refer to different instances of the Entree class.
The arrow operator (->), composed of a hyphen and a
greater-than sign, is your road to the properties (variables) and
methods (functions) inside an object. To access a property, put the
arrow after the object’s name and put the property after the arrow. To
call a method, put the method name after the arrow, followed by
the parentheses that indicate a function call.
Note that the arrow operator used to access properties and methods is
different from the operator that separates array keys and values in
array() or foreach(). The
array arrow has an equals sign: =>. The object
arrow has a hyphen: ->.
Assigning a value to a property works just like assigning a value to any other variable, but with the arrow syntax to indicate the property name. The expression $soup->name means “the name property inside the object instance that the $soup variable holds,” and the expression $sandwich->ingredients means “the ingredients property inside the object instance that the $sandwich variable holds.”
Inside the foreach() loop, each object’s hasIngredient() method gets called. The method is passed the name of an ingredient, and it returns whether or not that ingredient is in the object’s ingredient list. Here you can see how the special $this variable works. When $soup->hasIngredient() is called, $this refers to $soup inside the body of hasIngredient(). When $sandwich->hasIngredient() is called, $this refers to $sandwich. The $this variable doesn’t always refer to the same object instance, but instead refers to the instance the method is being called on. This means that when Example 6-2 runs, it prints:
Soup contains chicken. Sandwich contains chicken. Sandwich contains bread. Soup contains water.
In Example 6-2, when $ing is chicken, then both $soup->hasIngredient($ing) and $sandwich->hasIngredient($ing) return true. Both objects’ $ingredients properties contain an element with the value chicken. But only $soup->ingredients has water and only $sandwich->ingredients has bread. Neither object has lemon in its $ingredients property.
Classes can also contain static methods. These methods cannot use the $this variable since they do not get run in the context of a specific object instance, but on the class itself. Static methods are useful for behavior that is relevant to what the class is for, but not to any one object. Example 6-3 adds a static method to Entree that returns a list of possible entrée sizes.
classEntree{public$name;public$ingredients=array();publicfunctionhasIngredient($ingredient){returnin_array($ingredient,$this->ingredients);}publicstaticfunctiongetSizes(){returnarray('small','medium','large');}}
The declaration of the static method in Example 6-3 is similar to other method definitions, with the addition of the static keyword before function. To call a static method, you put :: between the class name and the method name instead of ->, as shown in Example 6-4.
$sizes=Entree::getSizes();
A class can have a special method, called a constructor, which is run when the object is created. Constructors typically handle setup and housekeeping tasks that make the object ready to use. For example, we can change the Entree class and give it a constructor. This constructor accepts two arguments: the name of the entrée and the ingredient list. By passing those values to the constructor, we avoid having to set the properties after the object is created. In PHP, the constructor method of a class is always called __construct(). Example 6-5 shows the changed class with its constructor method.
classEntree{public$name;public$ingredients=array();publicfunction__construct($name,$ingredients){$this->name=$name;$this->ingredients=$ingredients;}publicfunctionhasIngredient($ingredient){returnin_array($ingredient,$this->ingredients);}}
In Example 6-5, you can see that the __construct() method accepts two arguments and assigns their values to the properties of the class. The fact that the argument names match the property names is just a convenience—the PHP engine doesn’t require that they match. Inside a constructor, the $this keyword refers to the specific object instance being constructed.
To pass arguments to the constructor, treat the class name like a function name when you invoke the new operator by putting parentheses and argument values after it. Example 6-6 shows our class with the constructor in action by creating $soup and $sandwich objects identical to what we’ve used previously.
// Some soup with name and ingredients$soup=newEntree('Chicken Soup',array('chicken','water'));// A sandwich with name and ingredients$sandwich=newEntree('Chicken Sandwich',array('chicken','bread'));
The constructor is invoked by the new operator as part of what the PHP engine does to create a new object, but the constructor itself doesn’t create the object. This means that the constructor function doesn’t return a value and can’t use a return value to signal that something went wrong. That is a job for exceptions, discussed in the next section.
In Example 6-5, what happens if something other than an array is passed in as the $ingredients argument? As the code is written in Example 6-5, nothing! $this->ingredients is assigned the value of $ingredients no matter what it is. But if it’s not an array, this causes problems when hasIngredient() is called—that method assumes the $ingredients property is an array.
Constructors are great for verifying that supplied arguments are the right type or otherwise appropriate. But they need a way to complain if there is a problem. This is where an exception comes in. An exception is a special object that can be used to indicate that something exceptional has happened. Creating an exception interrupts the PHP engine and sets it on a different code path.
Example 6-7 modifies the Entree constructor to throw an exception if the $ingredients argument is not an array. (“Throwing” an exception means you use an exception tell the PHP engine that something went wrong.)
classEntree{public$name;public$ingredients=array();publicfunction__construct($name,$ingredients){if(!is_array($ingredients)){thrownewException('$ingredients must be an array');}$this->name=$name;$this->ingredients=$ingredients;}publicfunctionhasIngredient($ingredient){returnin_array($ingredient,$this->ingredients);}}
Exceptions are represented by the Exception class. The first argument to Exception’s constructor is a string describing what went wrong. So, the line throw new Exception('$ingredients must be an array'); creates a new Exception object and then hands it to the throw construct in order to interrupt the PHP engine.
If $ingredients is an array, then the code runs just as before. If it’s not an array, then the exception is thrown. Example 6-8 shows the creation of an Entree object with a bad $ingredients argument.
$drink=newEntree('Glass of Milk','milk');if($drink->hasIngredient('milk')){"Yummy!";}
Example 6-8 displays an error message like this (assuming the code is in a file named exception-use.php and the Entree class definition is in a file named construct-exception.php):
PHP Fatal error: Uncaught Exception: $ingredients must be an array
in construct-exception.php:9
Stack trace:
#0 exception-use.php(2): Entree->__construct('Glass of Milk', 'milk')
#1 {main}
thrown in construct-exception.php on line 9
In that error output, there are two separate things to recognize. The first is the error message from the PHP engine: PHP Fatal error: Uncaught exception 'Exception' with message '$ingredients must be an array' in construct-exception.php:9. This means that in line 9 of construct-exception.php (the file defining the Entree class), an exception was thrown. Because there was no additional code to deal with that exception (we’ll see how to do that shortly), it’s called “uncaught” and causes the PHP engine to come to a screaming halt—a “fatal” error that stops program execution immediately.
The second thing in that error output is a stack trace: a list of all the functions that were active when the PHP engine stopped. Here there’s just one: the Entree constructor that got called from new Entree. The {main} line in the stack trace represents the first level of program execution before anything else runs. You’ll always see that at the bottom of any stack trace.
It’s good that we prevented hasIngredient() from getting called so it doesn’t operate on a non-array of ingredients, but completely stopping the program with such a harsh error message is overkill. The flip side of throwing exceptions is catching them—grabbing the exception before the PHP engine gets it and bails out.
To handle an exception yourself, do two things:
try block.catch block after the potentially exception-throwing code in order to handle the problem.Example 6-9 adds try and catch blocks to deal with the exception.
try{$drink=newEntree('Glass of Milk','milk');if($drink->hasIngredient('milk')){"Yummy!";}}catch(Exception$e){"Couldn't create the drink: ".$e->getMessage();}
In Example 6-9, the try and catch blocks work together. Each of the statements inside the try block is run, stopping if an exception is encountered. If that happens, the PHP engine jumps down to the catch block, setting the variable $e to hold the Exception object that was created. The code inside the catch block uses the Exception class’s getMessage() method to retrieve the text of the message given to the exception when it was created. Example 6-9 prints:
Couldn't create the drink: $ingredients must be an array
One of the aspects of objects that make them so helpful for organizing your code is the notion of subclassing, which lets you reuse a class while adding some custom functionality. A subclass (sometimes called a child class) starts with all the methods and properties of an existing class (the parent class), but then can change them or add its own.
For example, consider an entrée that is not just a single dish but a combination of a few, such as a bowl of soup and a sandwich together. Our existing Entree class would be forced to model this either by treating “soup” and “sandwich” as ingredients or by enumerating all of the soup ingredients and sandwich ingredients as ingredients of this combo. Neither solution is ideal: soup and sandwich themselves are not ingredients, and reenumerating all the ingredients would mean we would need to update multiple places when any ingredient changed.
We can solve the problem more cleanly by making a subclass of Entree that expects to be given Entree object instances as ingredients and then modifying the subclass’s hasIngredient() method to inspect those object instances for ingredients. The code for this ComboMeal class is shown in Example 6-10.
classComboMealextendsEntree{publicfunctionhasIngredient($ingredient){foreach($this->ingredientsas$entree){if($entree->hasIngredient($ingredient)){returntrue;}}returnfalse;}}
In Example 6-10, the class name, ComboMeal, is followed by extends Entree. This tells the PHP engine that the ComboMeal class should inherit all of the methods and properties of the Entree class. To the PHP engine, it’s as if you retyped the definition of Entree inside the definition of ComboMeal, but you get that without actually having to do all that tedious typing. Then, the only things that need to be inside the curly braces of ComboMeal’s definition are changes or additions. In this case, the only change is a new hasIngredient() method. Instead of examining $this->ingredients as an array, it treats it as an array of Entree objects and calls the hasIngredient() method on each of those objects. If any of those calls return true, it means that one of the entrées in the combo has the specified ingredient, so ComboMeal’s hasIngredient() method returns true. If, after iterating through all of the entrées, nothing has returned true, then the method returns false, which means that no entrée has the ingredient in it. Example 6-11 shows the subclass at work.
// Some soup with name and ingredients$soup=newEntree('Chicken Soup',array('chicken','water'));// A sandwich with name and ingredients$sandwich=newEntree('Chicken Sandwich',array('chicken','bread'));// A combo meal$combo=newComboMeal('Soup + Sandwich',array($soup,$sandwich));foreach(['chicken','water','pickles']as$ing){if($combo->hasIngredient($ing)){"Something in the combo contains$ing.\n";}}
Because both the soup and the sandwich contain chicken, the soup contains water, but neither contains pickles, Example 6-11 prints:
Something in the combo contains chicken. Something in the combo contains water.
This works well, but we don’t have any guarantee that the items passed to ComboMeal’s constructor are really Entree objects. If they’re not, then invoking hasIngredient() on them could cause an error. To fix this, we need to add a custom constructor to ComboMeal that checks this condition and also invokes the regular Entree constructor so that the properties are set properly. A version of ComboMeal with this constructor is shown in Example 6-12.
classComboMealextendsEntree{publicfunction__construct($name,$entrees){parent::__construct($name,$entrees);foreach($entreesas$entree){if(!$entreeinstanceofEntree){thrownewException('Elements of $entrees must be Entree objects');}}}publicfunctionhasIngredient($ingredient){foreach($this->ingredientsas$entree){if($entree->hasIngredient($ingredient)){returntrue;}}returnfalse;}}
The constructor in Example 6-12 uses the special syntax parent::__construct() to refer to the constructor in Entree. Just as $this has a special meaning inside of object methods, so does parent. It refers to the class of which the current class is a subclass. Because ComboMeal extends Entree, parent inside of ComboMeal refers to Entree. So, parent::__construct() inside of ComboMeal refers to the __construct() method inside the Entree class.
In subclass constructors, it is important to remember that you have to call the parent constructor explicitly. If you leave out the call to parent::__construct(), the parent constructor never gets called and its presumably important behavior never gets executed by the PHP engine. In this case, Entree’s constructor makes sure that $ingredients is an array and sets the $name and $ingredients properties.
After the call to parent::__construct(), ComboMeal’s constructor ensures that each provided ingredient of the combo is itself an Entree object. It uses the instanceof operator for this. The expression $entree instanceof Entree evaluates to true if $entree refers to an object instance of the Entree class.1 If any of the provided ingredients (which, for a ComboMeal, are really entrées) are not Entree objects, then the code throws an exception.
The ComboMeal constructor in Example 6-12 does a great job of ensuring that a ComboMeal is only given instances of Entree to be its ingredients. But what happens after that? Subsequent code could change the value of the $ingredients property to anything—an array of non-Entrees, a number, or even false.
We prevent this problem by changing the visibility of the properties. Instead of public, we can label them as private or protected. These other visibility settings don’t change what code inside the class can do—it can always read or write its own properties. The private visibility prevents any code outside the class from accessing the property. The protected visibility means that the only code outside the class that can access the property is code in subclasses.
Example 6-13 shows a modified version of the Entree class in which the $name property is private and the $ingredients property is protected.
classEntree{private$name;protected$ingredients=array();/* Since $name is private, this provides a way to read it */publicfunctiongetName(){return$this->name;}publicfunction__construct($name,$ingredients){if(!is_array($ingredients)){thrownewException('$ingredients must be an array');}$this->name=$name;$this->ingredients=$ingredients;}publicfunctionhasIngredient($ingredient){returnin_array($ingredient,$this->ingredients);}}
Because $name is private in Example 6-13, there is no way to read or change it from code outside Entree. The added getName() method provides a way for non-Entree code to get the value of $name, though. This kind of method is called an accessor. It provides access to a property that would otherwise be forbidden. In this case, the combination of private visibility and an accessor that returns the property value lets any code read the value of $name, but nothing outside of Entree can change $name’s value once it’s been set.
The $ingredients property, on the other hand, is protected, which allows access to $ingredients from subclasses. This ensures that the hasIngredient() method in ComboMeal works properly.
The same visibility settings apply equally to methods as well as properties. Methods marked public may be invoked by any code. Methods marked private may be invoked only by other code inside the same class. Methods marked protected may be invoked only by other code inside the same class or inside subclasses.
Beginning with version 5.4, the PHP engine lets you organize your code into namespaces. Namespaces provide a way to group related code and ensure that names of classes that you’ve written don’t collide with identically named classes written by someone else.2
Getting comfortable with namespaces is important so you can incorporate packages written by others into your programs. Chapter 16 goes into detail about using the Composer package management system. This section familiarizes you with the syntax of namespaces.
Think of a namespace as a container that can hold class definitions or other namespaces. It’s a syntactic convenience, rather than providing new functionality. When you see the namespace keyword or some backslashes in what appears to be a class name, you’ve encountered a PHP namespace.
To define a class inside a particular namespace, use the namespace keyword at the top of a file with a namespace name. Then, a class definition later in the file will define the class inside that namespace. Example 6-14 defines a Fruit class inside the Tiny namespace.
namespaceTiny;classFruit{publicstaticfunctionmunch($bite){"Here is a tiny munch of$bite.";}}
To use a class defined in a namespace, you need to incorporate the namespace into how you refer to the class. The most unambiguous way to do this is to begin with \ (the top-level namespace), then write the name of the namespace the class is in, then add another \, then write the class name. For example, to invoke munch() on the Fruit class defined in Example 6-14, write:
\Tiny\Fruit::munch("banana");
Namespaces can also hold other namespaces. If Example 6-14 began with namespace Tiny\Eating;, then you’d refer to the class as \Tiny\Eating\Fruit.
Without that leading \, how a reference to a class gets resolved depends on the current namespace—whatever namespace is active at the time of the reference. In a PHP file with no namespace declaration at the top, the current namespace is the top-level namespace. Class names behave like regular class names that you’ve encountered so far without namespaces. The namespace keyword, however, changes the current namespace. A declaration of namespace Tiny; changes the current namespace to Tiny. That’s why the class Fruit definition in Example 6-14 puts the Fruit class inside the Tiny namespace.
However, this also means that any other class name reference in that file is resolved relative to the Tiny namespace. A method inside the Tiny\Fruit class that contains the code $soup = new Entree('Chicken Soup', array('chicken','water')); tells the PHP engine to look for an Entree class inside the Tiny namespace. It’s as if the code were written as $soup = new \Tiny\Entree('Chicken Soup', array('chicken','water'));. To unambiguously refer to a class in the top-level namespace, you need a leading \ before the class name.
Typing all those backslashes and namespace names over and over again is painful. The PHP engine gives you the use keyword to simplify things. Example 6-15 shows how to use use.
useTiny\Eating\FruitasSnack;useTiny\Fruit;// This calls \Tiny\Eating\Fruit::munch();Snack::munch("strawberry");// This calls \Tiny\Fruit::munch();Fruit::munch("orange");
Writing use Tiny\Eating\Fruit as Snack; tells the PHP engine, “For the rest of this file, when I say Snack as a class name, I really mean \Tiny\Eating\Fruit.” Without the as, the PHP engine infers the “nickname” for the class from the last element of what is given to use. So, use Tiny\Fruit; tells the PHP engine, “For the rest of this file, when I say Fruit as a class name, I really mean \Tiny\Fruit.”
These kinds of use declarations are especially helpful with many modern PHP frameworks that put their various classes into namespaces and subnamespaces. With a few use lines at the top of your file, you can transform verbose incantations such as \Symfony\Component\HttpFoundation\Response to a more concise Response.
This chapter covered:
new operatorstatic methodIngredient. Each instance of this class represents a single ingredient. The instance should keep track of an ingredient’s name and its cost.IngredientCost class that changes the cost of an ingredient.Entree class used in this chapter that accepts Ingredient objects instead of string ingredient names to specify the ingredients. Give your Entree subclass a method that returns the total cost of the entrée.Ingredient class into its own namespace and modify your other code that uses IngredientCost to work properly.1 The instanceof operator also evaluates to true if the provided object is a subclass of the provided class name. This code will work, for example, with combo meals made up of other combo meals.
2 Namespaces cover functions and some other things that are not classes, but this section just explores namespaces with classes.