Objects and classes lie at the heart of this book, and, since the introduction of PHP 5 over a decade ago, they have lain at the heart of PHP, too. In this chapter, I establish the groundwork for more in-depth coverage of objects and design by examining PHP’s core object-oriented features. If you are new to object-oriented programming, you should read this chapter carefully.
Classes and objects: Declaring classes and instantiating objects
Constructor methods: Automating the setup of your objects
Primitive and class types: Why type matters
Inheritance: Why we need inheritance and how to use it
Visibility: Streamlining your object interfaces and protecting your methods and properties from meddling
Classes and Objects
The first barrier to understanding object-oriented programming is the strange and wonderful relationship between the class and the object. For many people, it is this relationship that represents the first moment of revelation, the first flash of object-oriented excitement. So let’s not skimp on the fundamentals.
A First Class
Classes are often described in terms of objects. This is interesting, because objects are often described in terms of classes. This circularity can make the first steps in object-oriented programming hard going. Because it’s classes that shape objects, we should begin by defining a class.
The ShopProduct class in the example is already a legal class, although it is not terribly useful yet. I have done something quite significant, however. I have defined a type; that is, I have created a category of data that I can use in my scripts. The power of this should become clearer as you work through the chapter.
A First Object (or Two)
If a class is a template for generating objects, it follows that an object is data that has been structured according to the template defined in a class. An object is said to be an instance of its class. It is of the type defined by the class.
The new operator is invoked with a class name as its only operand and returns an instance of that class; in our example, it generates a ShopProduct object.
I have used the ShopProduct class as a template to generate two ShopProduct objects. Although they are functionally identical (i.e., empty), $product1 and $product2 are different objects of the same type generated from a single class.
In ancient versions of PHP (up to version 5.1), you could print an object directly. This casted the object to a string containing the object’s ID. From PHP 5.2 onward, the language no longer supported this magic, and any attempt to treat an object as a string now causes an error unless a method named __toString() is defined in the object’s class. I look at methods later in this chapter, and I cover __toString() in Chapter 4.
By passing the objects to var_dump(), I extract useful information including, after the hash sign, each object’s internal identifier.
In order to make these objects more interesting, I can amend the ShopProduct class to support special data fields called properties.
Setting Properties in a Class
Classes can define special variables called properties. A property, also known as a member variable, holds data that can vary from object to object. So in the case of ShopProduct objects, you may wish to manipulate title and price fields, for example.
A property in a class looks similar to a standard variable except that, in declaring a property, you must precede the property variable with a visibility keyword. This can be public, protected, or private, and it determines the location in your code from which the property can be accessed. Public properties are accessible outside the class, for example, and private properties can only be accessed by code within the class.
As you can see, I set up four properties, assigning a default value to each of them. Any objects I instantiate from the ShopProduct class will now be prepopulated with default data. The public keyword in each property declaration ensures that I can access the property from outside of the object context.
By declaring and setting the $title property in the ShopProduct class, I ensure that all ShopProduct objects have this property when first created. This means code that uses this class can work with ShopProduct objects based on that assumption. Because I can reset it, though, the value of $title may vary from object to object.
Code that uses a class, function, or method is often described as the class’s, function’s, or method’s client or as client code. You will see this term frequently in the coming chapters.
However, this method of assigning properties to objects is not considered good practice in object-oriented programming.
Why is it bad practice to set properties dynamically? When you create a class, you define a type. You inform the world that your class (and any object instantiated from it) consists of a particular set of fields and functions. If your ShopProduct class defines a $title property, then any code that works with ShopProduct objects can proceed on the assumption that a $title property will be available. There can be no guarantees about properties that have been dynamically set, though.
As far as the PHP engine is concerned, this code is perfectly legal, and I would not be warned. When I come to print the author’s name, though, I will get unexpected results.
Another problem is that my class is altogether too relaxed. I am not forced to set a title, a price, or producer names. Client code can be sure that these properties exist, but is likely to be confronted with default values as often as not. Ideally, I would like to encourage anyone who instantiates a ShopProduct object to set meaningful property values.
Finally, I have to jump through hoops to do something that I will probably want to do quite often. As we have seen, printing the full author name is a tiresome process.
It would be nice to have the object handle such drudgery on my behalf.
All of these problems can be addressed by giving the ShopProduct object its own set of functions that can be used to manipulate property data from within the object context.
Working with Methods
Unlike functions, methods must be declared in the body of a class. They can also accept a number of qualifiers, including a visibility keyword. Like properties, methods can be declared public, protected, or private. By declaring a method public, you ensure that it can be invoked from outside of the current object. If you omit the visibility keyword in your method declaration, the method will be declared public implicitly. It is considered good practice, however, to declare visibility explicitly for all methods (I will return to method modifiers later in the chapter).
In Chapter 15, I cover rules for best practices in code. The coding style standard PSR-12 requires that visibility is declared for all methods.
I add the getProducer() method to the ShopProduct class. Notice that I declare getProducer() public, which means it can be called from outside the class.
This translates to the following:
the $producerFirstName property of the current instance
So the getProducer() method combines and returns the $producerFirstName and $producerMainName properties, saving me from the chore of performing this task every time I need to quote the full producer name.
This has improved the class a little. I am still stuck with a great deal of unwanted flexibility, though. I rely on the client coder to change a ShopProduct object’s properties from their default values. This is problematic in two ways. First, it takes five lines to properly initialize a ShopProduct object, and no coder will thank you for that. Second, I have no way of ensuring that any of the properties are set when a ShopProduct object is initialized.
What I need is a method that is called automatically when an object is instantiated from a class.
Creating a Constructor Method
A constructor method is invoked when an object is created. You can use it to set things up, ensuring that essential properties are assigned values and any necessary preliminary work is completed.
In versions previous to PHP 5, a constructor method took on the name of the class that enclosed it. So the ShopProduct class would use a ShopProduct() method as its constructor. This was deprecated as of PHP 7 and no longer works at all as of PHP 8. Name your constructor method __construct().
Note that the method name begins with two underscore characters. You will see this naming convention for many other special methods in PHP classes. Here, I define a constructor for the ShopProduct class :
Built-in methods which begin this way are known as magic methods because they are automatically invoked in specific circumstances. You can read more about them in the PHP manual at www.php.net/manual/en/language.oop5.magic.php. Although it is not illegal to do so, because double underscores have such a specific connotation, it is a good idea to avoid using them in your own custom methods.
Any arguments supplied are passed to the constructor. So in my example, I pass the title, the first name, the main name, and the product price to the constructor. The constructor method uses the pseudo-variable $this to assign values to each of the object’s properties.
A ShopProduct object is now easier to instantiate and safer to use. Instantiation and setup are completed in a single statement. Any code that uses a ShopProduct object can be reasonably sure that all its properties are initialized.
You can leave a property uninitialized without error. But any attempt to access that property will then result in a fatal error.
Constructor Property Promotion
Both declaration of and assignment to the properties in the constructor method signature are handled implicitly. By reducing repetition, this also reduces the chance of bugs creeping into code. By making the class more compact, it makes it easier for those reading source code to focus on the logic.
Constructor property promotion was introduced in PHP 8. If your project is still running PHP 7, then you should hold off from taking advantage of the new syntax.
Predictability is an important aspect of object-oriented programming. You should design your classes so that users of objects can be sure of their features. One way you can make an object safe is to render predictable the types of data it holds in its properties. One might ensure that a $name property is always made up of character data, for example. But how can you achieve this if property data is passed in from outside the class? In the next section, I examine a mechanism you can use to enforce object types in method declarations.
Default Arguments and Named Arguments
Default argument values can make working with methods more convenient, but, as is so often the way, they can also cause unintended complications. What would happen to my nice compact constructor call if I wanted to provide a price but would still like the producer names to fall back to their defaults? Prior to PHP 8, I would be stuck. I would have to provide the empty producer names in order to specify the price. That brings us full circle. And I would also need to work out what kind of values the constructor expects for empty producer name values. Should I pass empty strings? Or null values? Far from saving work, my support for default values may well have sown confusion.
Note the syntax here: I tell PHP I want to set the $price argument to 0.7 by first specifying the argument name, price, then a colon, and then the value I want to provide. Because I have used named arguments, their order in the call is no longer relevant, and I no longer need to provide the empty producer name values.
Arguments and Types
A type determines the way data can be managed in your scripts. You use the string type to display character data, for example, and manipulate such data with string functions. Integers are used in mathematical expressions, Booleans are used in test expressions, and so on. These categories are known as primitive types. On a higher level, though, a class defines a type. A ShopProduct object, therefore, belongs to the primitive type object, but it also belongs to the ShopProduct class type. In this section, I will look at types of both kinds in relation to class methods.
Method and function definitions do not necessarily require that an argument should be of a particular type. This is both a curse and a blessing. The fact that an argument can be of any type offers you flexibility. You can build methods that respond intelligently to different data types, tailoring functionality to changing circumstances. This flexibility can also cause ambiguity to creep into code when a method body expects an argument to hold one type but gets another.
Primitive Types
PHP is a loosely typed language. This means that there is no necessity for a variable to be declared to hold a particular data type. The variable $number could hold the value 2 and the string "two" within the same scope. In strongly typed languages, such as C or Java, you must declare the type of a variable before assigning a value to it, and, of course, the value must be of the specified type.
Primitive Types and Checking Functions in PHP
Type-Checking Function | Type | Description |
---|---|---|
is_bool() | Boolean | One of the two special values true or false |
is_integer() | Integer | A whole number. Alias of is_int() and is_long() |
is_float() | Float | A floating-point number (a number with a decimal point). Alias of is_double() |
is_string() | String | Character data |
is_object() | Object | An object |
is_resource() | Resource | A handle for identifying and working with external resources such as databases or files |
is_array() | Array | An array |
is_null() | Null | An unassigned value |
Checking the type of a variable can be particularly important when you work with method and function arguments.
Primitive Types: An Example
You need to keep a close eye on type in your code. Here’s an example of one of the many type-related problems that you could encounter.
Imagine that you are extracting configuration settings from an XML file. The <resolvedomains></resolvedomains> XML element tells your application whether it should attempt to resolve IP addresses to domain names, a useful but relatively expensive process.
Of course, the AddressManager class could do with some improvement. It’s not very useful to hard-code IP addresses into a class, for example. Nevertheless, the outputAddresses() method loops through the $addresses array property, printing each element. If the $resolve argument variable itself resolves to true, the method outputs the domain name, as well as the IP address.
The code fragment uses the SimpleXML API to acquire a value for the resolvedomains element. In this example, I know that this value is the text element "false", and I cast it to a string as the SimpleXML documentation suggests I should.
There are a number of approaches you might take to fix this.
There are good design reasons for avoiding an approach like this, however. Generally speaking, it is better to provide a clear and strict interface for a method or function than it is to offer a fuzzily forgiving one. Fuzzy and forgiving functions and methods can promote confusion and thereby breed bugs.
This is a reasonable approach, assuming your client coders are diligent readers of documentation (or use clever editors that recognize annotations of this sort).
This approach can be used to force client code to provide the correct data type in the $resolve argument or to issue a warning.
In the next section, “Type Declarations: Object Types,” I will describe a much better way of constraining the type of arguments passed to methods and functions.
Converting a string argument on the client’s behalf would be friendly but would probably present other problems. In providing a conversion mechanism, you second-guess the context and intent of the client. By enforcing the Boolean data type, on the other hand, you leave the client to decide whether to map strings to Boolean values and determine which word should map to true or false. The outputAddresses() method , meanwhile, concentrates on the task it is designed to perform. This emphasis on performing a specific task in deliberate ignorance of the wider context is an important principle in object-oriented programming, and I will return to it frequently throughout the book.
In fact, your strategies for dealing with argument types will depend on the seriousness of any potential bugs on the one hand and the benefits of flexibility on the other. PHP casts most primitive values for you, depending on context. Numbers in strings are converted to their integer or floating-point equivalents when used in a mathematical expression, for example. So your code might be naturally forgiving of type errors.
On the whole, however, it is best to err on the side of strictness when it comes to both object and primitive types. Luckily, PHP 8 provides more tools than ever before to enforce type safety.
Some Other Type-Checking Functions
Pseudo-type-Checking Functions
Function | Description |
---|---|
is_countable() | An array or an object that can be passed to the count() function |
is_iterable() | A traversable data structure—that is, one that can be looped through using foreach |
is_callable() | Code that can be invoked—often an anonymous function or a function name |
is_numeric() | Either an int, a long, or a string which can be resolved to a number |
The functions described in Table 3-2 do not check for specific types so much as ways you can treat the values you test. If is_callable() returns true for a variable, for example, you know that you can treat it like a function or method and invoke it. Similarly, you can loop through a value that passes the is_iterable() test—even though it may be a special kind of object rather than an array.
Type Declarations: Object Types
Just as an argument variable can contain any primitive type, by default it can contain an object of any type. This flexibility has its uses, but can present problems in the context of a method definition.
The ShopProductWriter class contains a single method, write(). The write() method accepts a ShopProduct object and uses its properties and methods to construct and print a summary string. I used the name of the argument variable, $shopProduct, as a signal that the method expects a ShopProduct object, but I did not enforce this. That means I could be passed an unexpected object or primitive type and be none the wiser until I begin trying to work with the $shopProduct argument. By that time, my code may already have acted on the assumption that it has been passed a genuine ShopProduct object.
You might wonder why I didn’t add the write() method directly to ShopProduct. The reason lies with areas of responsibility. The ShopProduct class is responsible for managing product data; the ShopProductWriter is responsible for writing it. You will begin to see why this division of labor can be useful as you read this chapter.
Now the write() method will only accept the $shopProduct argument if it contains an object of type ShopProduct.
In the TypeError example output, you might have noticed that the classes referenced included much additional information. The Wrong class is quoted as poppch03atch08 Wrong, for example. These are examples of namespaces, and you will encounter them in great detail in Chapter 4.
This saves me from having to test the type of the argument before I work with it. It also makes the method signature much clearer for the client coder. She can see the requirements of the write() method at a glance. She does not have to worry about some obscure bug arising from a type error because the declaration is rigidly enforced.
Even though this automated type checking is a great way of preventing bugs, it is important to understand that type declarations are checked at runtime. This means that a class declaration will only report an error at the moment that an unwanted object is passed to the method. If a call to write() is buried in a conditional clause that only runs on Christmas morning, you may find yourself working the holiday if you haven’t checked your code carefully.
Type Declarations: Primitive Types
Up until the release of PHP 7, it was only possible to constrain objects and a couple of other types (callable and array). PHP 7 at last introduced scalar type declarations. This allows you to enforce the Boolean, string, integer, and float types in your argument list.
Because of implicit casting, it is functionally identical to one that passes the Boolean value true.
A strict_types declaration applies to the file from which a call is made, and not to the file in which a function or method is implemented. So it’s up to client code to enforce strictness.
mixed Types
In the second version, I declared that the $value argument to add() would accept mixed—in other words, any type from array, bool, callable, int, float, null, object, resource, or string. So declaring a mixed $value is the same as leaving $value without a type declaration in an argument list. So why bother with the mixed declaration at all? In essence, you are declaring that the argument intentionally accepts any value. A bare argument might be intended to accept any value—or it may have been left without a type declaration because the code author was lazy. mixed removes doubt and uncertainty, and for that reason it is useful.
Type Declarations
Type Declaration | Since | Description |
---|---|---|
array | 5.1 | An array. Can default to null or an array |
int | 7.0 | An integer. Can default to null or an integer |
float | 7.0 | A floating-point number (a number with a decimal point). An integer will be accepted—even with strict mode enabled. Can default to null, a float, or an integer |
callable | 5.4 | Callable code (such as an anonymous function). Can default to null |
bool | 7.0 | A Boolean. Can default to null or a Boolean |
string | 5.0 | Character data. Can default to null or a string |
self | 5.0 | A reference to the containing class |
[a class type] | 5.0 | The type of a class or interface. Can default to null |
iterable | 7.1 | Can be traversed with foreach (not necessarily an array—could implement Traversable) |
object | 7.2 | An object |
mixed | 8.0 | Explicit notification that the value can be of any type |
Union Types
In fact, rather than return false, we would likely throw an exception. You can read more about exceptions in Chapter 4.
Although this manual checking gets the job done, it is unwieldy and hard to read. Luckily, PHP 8 introduced a new feature: union types which allow you to combine two or more types separated by a pipe symbol to make a composite type declaration.
If I now attempt to set $value to anything other than a float or a Boolean, I will trip a now-familiar TypeError.
This is more useful than the union ShopProduct|bool because I do not want to accept true in any scenario.
Union types were added in PHP 8.
Nullable Types
When I described class type declarations, I implied that types and classes are synonymous. There is a key difference between the two, however. When you define a class, you also define a type, but a type can describe an entire family of classes. The mechanism by which different classes can be grouped together under a type is called inheritance. I discuss inheritance in the next section.
Return Type Declarations
Because the return value is enforced in this way, any code that calls this method can treat its return value as an integer with assurance.
Inheritance
Inheritance is the means by which one or more classes can be derived from a base class.
A class that inherits from another is said to be a subclass of it. This relationship is often described in terms of parents and children. A child class is derived from and inherits characteristics from the parent. These characteristics consist of both properties and methods. The child class will typically add new functionality to that provided by its parent (also known as a superclass); for this reason, a child class is said to extend its parent.
Before I dive into the syntax of inheritance, I’ll examine the problems it can help you to solve.
The Inheritance Problem
Separating the producer name into two parts works well with both books and CDs. I want to be able to sort on “Alabama 3” and “Cather”, not on “The” and “Willa”. Laziness is an excellent design strategy, so there is no need to worry about using ShopProduct for more than one kind of product at this stage.
If I add some new requirements to my example, however, things rapidly become more complicated. Imagine, for example, that you need to represent data specific to books and CDs. For CDs, you must store the total playing time; for books, the total number of pages. There could be any number of other differences, but this will serve to illustrate the issue.
How can I extend my example to accommodate these changes? Two options immediately present themselves. First, I could throw all the data into the ShopProduct class. Second, I could split ShopProduct into two separate classes.
I have provided method access to the $numPages and $playLength properties to illustrate the divergent forces at work here. An object instantiated from this class will include a redundant method and, for a CD, must be instantiated using an unnecessary constructor argument: a CD will store information and functionality relating to book pages, and a book will support play-length data. This is probably something you could live with right now. But what would happen if I added more product types, each with its own methods, and then added more methods for each type? Our class would become increasingly complex and hard to manage.
So forcing fields that don’t belong together into a single class leads to bloated objects with redundant properties and methods.
The problem doesn’t end with data, either. I run into difficulties with functionality as well. Consider a method that summarizes a product. The sales department has requested a clear summary line for use in invoices. They want me to include the playing time for CDs and a page count for books, so I will be forced to provide different implementations for each type. I could try using a flag to keep track of the object’s format.
In order to set the $type property, I could test the $numPages argument to the constructor. Still, once again, the ShopProduct class has become more complex than necessary. As I add more differences to my formats, or add new formats, these functional differences will become even harder to manage. Perhaps I should try another approach to this problem.
I have addressed the complexity issue, but at a cost. I can now create a getSummaryLine() method for each format without having to test a flag. Neither class maintains fields or methods that are not relevant to it.
The cost lies in duplication. The getProducerName() method is exactly the same in each class. Each constructor sets a number of identical properties in the same way. This is another unpleasant odor you should train yourself to sniff out.
If I need the getProducer() methods to behave identically for each class, any changes I make to one implementation will need to be made for the other. Without care, the classes will soon slip out of synchronization.
Even if I am confident that I can maintain the duplication, my worries are not over. I now have two types rather than one.
Notice the instanceof operator in the example; instanceof resolves to true if the object in the left-hand operand is of the type represented by the right-hand operand.
Once again, I have been forced to include a new layer of complexity. Not only do I have to test the $shopProduct argument against two types in the write() method , but I have to trust that each type will continue to support the same fields and methods as the other. It was all much neater when I simply demanded a single type because I could use a class type declaration and because I could be confident that the ShopProduct class supported a particular interface.
The CD and book aspects of the ShopProduct class don’t work well together but can’t live apart, it seems. I want to work with books and CDs as a single type while providing a separate implementation for each format. I want to provide common functionality in one place to avoid duplication, but allow each format to handle some method calls differently. I need to use inheritance.
Working with Inheritance
The first step in building an inheritance tree is to find the elements of the base class that don’t fit together or that need to be handled differently.
I know that the getPlayLength() and getNumberOfPages() methods do not belong together. I also know that I need to create different implementations for the getSummaryLine() method .
To create a child class, you must use the extends keyword in the class declaration. In the example, I created two new classes, BookProduct and CdProduct. Both extend the ShopProduct class.
So both the child classes inherit the behavior of the common parent. You can treat a BookProduct object as if it were a ShopProduct object. You can pass a BookProduct or CdProduct object to the ShopProductWriter class’s write() method , and all will work as expected.
Notice that both the CdProduct and BookProduct classes override the getSummaryLine() method, providing their own implementation. Derived classes can extend but also alter the functionality of their parents.
The superclass’s implementation of this method might seem redundant because it is overridden by both its children. Nevertheless, it provides basic functionality that new child classes might use. The method’s presence also provides a guarantee to client code that all ShopProduct objects will provide a getSummaryLine() method . Later on, you will see how it is possible to make this promise in a base class without providing any implementation at all. Each child ShopProduct class inherits its parent’s properties. Both BookProduct and CdProduct access the $title property in their versions of getSummaryLine().
Inheritance can be a difficult concept to grasp at first. By defining a class that extends another, you ensure that an object instantiated from it is defined by the characteristics of first the child and then the parent class. Another way of thinking about this is in terms of searching. When I invoke $product2->getProducer(), there is no such method to be found in the CdProduct class, and the invocation falls through to the default implementation in ShopProduct. When I invoke $product2->getSummaryLine(), on the other hand, the getSummaryLine() method is found in CdProduct and invoked.
The same is true of property accesses. When I access $title in the BookProduct class’s getSummaryLine() method, the property is not found in the BookProduct class. It is acquired instead from the parent class, from ShopProduct. The $title property applies equally to both subclasses, and therefore it belongs in the superclass.
A quick look at the ShopProduct constructor, however, shows that I am still managing data in the base class that should be handled by its children. The BookProduct class should handle the $numPages argument and property, and the CdProduct class should handle the $playLength argument and property. To make this work, I will define constructor methods in each of the child classes.
Constructors and Inheritance
When you define a constructor in a child class, you become responsible for passing any arguments on to the parent. If you fail to do this, you can end up with a partially constructed object.
To invoke a method in a parent class, you must first find a way of referring to the class itself: a handle. PHP provides us with the parent keyword for this purpose.
I cover the scope resolution operator (::) in more detail in Chapter 4.
Each child class invokes the constructor of its parent before setting its own properties. The base class now knows only about its own data. Child classes are generally specializations of their parents. As a rule of thumb, you should avoid giving parent classes any special knowledge about their children.
Prior to PHP 5, constructors took on the name of the enclosing class. The new unified constructors use the name __construct(). Using the old syntax, a call to a parent constructor would tie you to that particular class: parent::ShopProduct();. The old constructor syntax was deprecated in PHP 7.0 and removed altogether in PHP 8.
Invoking an Overridden Method
I set up the core functionality for the getSummaryLine() method in the ShopProduct base class.
Rather than reproduce this in the CdProduct and BookProduct subclasses, I simply call the parent method before proceeding to add more data to the summary string.
Now that you have seen the basics of inheritance, I will reexamine property and method visibility in light of the full picture.
Public, Private, and Protected: Managing Access to Your Classes
So far, I have declared all properties public. Public access was the default setting for methods and for properties if you used the old var keyword in your property declaration.
var was deprecated in PHP 5 and will likely be completely removed from the language in future.
Public properties and methods can be accessed from any context.
A private method or property can only be accessed from within the enclosing class. Even subclasses have no access.
A protected method or property can only be accessed from within either the enclosing class or from a subclass. No external code is granted access.
So how is this useful to us? Visibility keywords allow you to expose only those aspects of a class that are required by a client. This sets a clear interface for your object.
This will print the raw price and not the discount-adjusted price you wish to present. You can put a stop to this straightaway by making the $price property private. This will prevent direct access, forcing clients to use the getPrice() method. Any attempt from outside the ShopProduct class to access the $price property will fail. As far as the wider world is concerned, this property has ceased to exist.
As the private $price property is declared in the ShopProduct class and not BookProduct, the attempt to access it here will fail. The solution to this problem is to declare the $price variable as protected, thereby granting access to descendant classes. Remember that a protected property or method cannot be accessed from outside the class hierarchy in which it was declared. It can only be accessed from within its originating class or from within children of the originating class.
As a general rule, err on the side of privacy. Make properties private or protected at first and relax your restriction only as needed. Many (if not most) methods in your classes will be public, but once again, if in doubt, lock it down. A method that provides local functionality for other methods in your class has no relevance to your class’s users. Make it private or protected.
Accessor Methods
Even when client programmers need to work with values held by your class, it is often a good idea to deny direct access to properties, providing methods instead that relay the needed values. Such methods are known as accessors or getters and setters.
You have already seen one benefit afforded by accessor methods. You can use an accessor to filter a property value according to circumstances, as was illustrated by the getPrice() method .
It’s now impossible for external code to damage the $products property. All access must be via the addProduct() method , and the class type declaration I use in the method declaration ensures that only ShopProduct objects can be added to the array property.
Typed Properties
Because the $x and $y properties are private, they can only be set via the setVals() method—and because setVals() will only accept integer values, you can be sure that $x and $y always contain integers.
Of course, because these properties are set private, the only way they can be accessed is through getter or accessor methods.
I have made the properties $x and $y public and used type declaration to constrain their types. Because of this, I can choose, if I want, to get rid of the setVals() method without sacrificing control. I also no longer need the getX() and getY() methods. Point is now an exceptionally simple class, but, even with both its properties public, it offers the world guarantees about the data it holds.
Union types can also be used in type property declarations.
The ShopProduct Classes
So, all properties are either private or protected in this version of the ShopProduct family. I have added a number of accessor methods to round things off.
Summary
This chapter covered a lot of ground, taking a class from an empty implementation through to a fully featured inheritance hierarchy. You took in some design issues, particularly with regard to type and inheritance. You saw PHP’s support for visibility and explored some of its uses. In the next chapter, I will show you more of PHP’s object-oriented features.