You have already seen how class type hinting and access control give you more control over a class’s interface. In this chapter I will delve deeper into PHP’s object-oriented features. This chapter will cover several subjects:
- Static methods and properties: Accessing data and functionality through classes rather than objects
- Abstract classes and interfaces: Separating design from implementation
- Traits: Sharing implementation between class hierarchies
- Error handling: Introducing exceptions
- Final classes and methods: Limiting inheritance
- Interceptor methods: Automating delegation
- Destructor methods: Cleaning up after your objects
- Cloning objects: Making object copies
- Resolving objects to strings: Creating a summary method
- Callbacks: Adding functionality to components with anonymous functions and classes
Static Methods and Properties
All of the examples in the previous chapter worked with objects. I characterized classes as templates from which objects are produced and objects as active instances of classes—the things whose methods you invoke and whose properties you access. I implied that in object-oriented programming the real work is done by instances of classes. Classes after all are merely templates for objects.
In fact it is not that simple. You can access both methods and properties in the context of a class rather than that of an object. Such methods and properties are “static” and must be declared as such by using the static
keyword:
class StaticExample {
public static int $aNum = 0;
public static function sayHello(): void {
print "hello";
}
}
Static methods are functions with class scope. They cannot themselves access any normal properties in the class because these would belong to an object; however, they can access static properties. If you change a static property, all instances of that class are able to access the new value.
Because you access a static element via a class and not an instance, you do not need a variable that references an object. Instead, you use the class name in conjunction with ::
as in this example:
print StaticExample::$aNum;
StaticExample::sayHello();
This syntax should be familiar from the previous chapter. I used ::
in conjunction with parent
to access an overridden method. Now, as then, I am accessing class rather than object data. Class code can use the parent
keyword to access a superclass without using its class name. To access a static method or property from within the same class (rather than from a child), I would use the self
keyword. self
is to classes what the $this
pseudo-variable is to objects. So from outside the StaticExample
class, I access the $aNum
property using its class name:
StaticExample::$aNum;
From within a class, I can use the self
keyword:
class StaticExample2 {
public static int $aNum = 0;
public static function sayHello(): void {
self::$aNum++;
print "hello (" . self::$aNum . ")\n";
}
}
Note: Making a method call using parent
is the only circumstance in which you should use a static reference to a non-static method. Unless you are accessing an overridden method, you should only ever use ::
to access a method or property that has been explicitly declared static. In documentation, however, you will often see static syntax used to refer to a method or property. This does not mean that the item in question is necessarily static, just that it belongs to a certain class. The write()
method of the ShopProductWriter
class might be referred to as ShopProductWriter::write()
for example even though the write()
method is not static. You will see this syntax here when that level of specificity is appropriate.
By definition, static methods and properties are invoked on classes and not objects. For this reason, they are often referred to as class variables and properties. As a consequence of this class orientation, you cannot use the $this
pseudo-variable inside a static method.
So why would you use a static method or property? Static elements have a number of characteristics that can be useful. First, they are available from anywhere in your script (assuming that you have access to the class). This means you can access functionality without needing to pass an instance of the class from object to object or, worse, storing an instance in a global variable. Second, a static property is available to every instance of a class, so you can set values that you want to be available to all members of a type. Finally, the fact that you don’t need an instance to access a static property or method can save you from instantiating an object purely to get at a simple function.
To illustrate this, I will build a static method for the ShopProduct
class that automates the instantiation of ShopProduct
objects. Using SQLite, I might define a products
table like this:
CREATE TABLE products (
id INTEGER PRIMARY KEY AUTOINCREMENT,
type TEXT,
firstname TEXT,
mainname TEXT,
title TEXT,
price float,
numpages int,
playlength int,
discount int
);
Now I want to build a getInstance()
method that accepts a row ID and PDO
object, uses them to acquire a database row, and then returns a ShopProduct
object. I can add these methods to the ShopProduct
class I created in the previous chapter. As you probably know, PDO
stands for PHP Data Object
. The PDO class provides a common interface to different database applications:
class ShopProduct {
private int $id = 0;
// ...
public function setID(int $id): void {
$this->id = $id;
}
// ...
public static function getInstance(int $id, \PDO $pdo): ShopProduct {
$stmt = $pdo->prepare("select * from products where id=?");
$result = $stmt->execute([$id]);
$row = $stmt->fetch();
if (empty($row)) {
return null;
}
if ($row['type'] == "book") {
$product = new BookProduct(
$row['title'],
$row['firstname'],
$row['mainname'],
(float) $row['price'],
(int) $row['numpages']
);
} elseif ($row['type'] == "cd") {
$product = new CdProduct(
$row['title'],
$row['firstname'],
$row['mainname'],
(float) $row['price'],
(int) $row['playlength']
);
} else {
$firstname = (is_null($row['firstname'])) ? "" : $row['firstname'];
$product = new ShopProduct(
$row['title'],
$firstname,
$row['mainname'],
(float) $row['price']
);
}
$product->setId((int) $row['id']);
$product->setDiscount((int) $row['discount']);
return $product;
}
}
As you can see, the getInstance()
method returns a ShopProduct
object and, based on a type flag, is smart enough to work out the precise specialization it should instantiate. I have omitted any error handling to keep the example compact. In a real-world version of this, for example, I would not be so trusting as to assume that the provided PDO
object was initialized to talk to the correct database. In fact, I would probably wrap the PDO
with a class that would guarantee this behavior. You can read more about object-oriented coding and databases in Chapter 13.
This method is more useful in a class context than an object context. It lets you convert raw data from the database into an object easily without requiring that you have a ShopProduct
object to start with. The method does not use any instance properties or methods, so there is no reason why it should not be declared static. Given a valid PDO
object, I can invoke the method from anywhere in an application:
$dsn = "sqlite:/tmp/products.sqlite3";
$pdo = new \PDO($dsn, null, null);
$pdo->setAttribute(\PDO::ATTR_ERRMODE, \PDO::ERRMODE_EXCEPTION);
$obj = ShopProduct::getInstance(1, $pdo);
Methods like this act as “factories” in that they take raw materials (such as row data or configuration information) and use them to produce objects. The term factory
is applied to code designed to generate object instances. You will encounter factory examples again in future chapters.
In some ways, of course, this example poses as many problems as it solves. Although I make the ShopProduct::getInstance()
method accessible from anywhere in a system without the need for a ShopProduct
instance, I also demand that client code provides a PDO object. Where is this to be found? And is it really good practice for a parent class to have such intimate knowledge of its children? (Hint: No, it is not.) Problems of this kind—where to acquire key objects and values and how much classes should know about one another—are very common in object-oriented programming. I examine various approaches to object generation in Chapter 9.
Constant Properties
Some properties should not be changed. The Answer to Life, the Universe, and Everything is 42, and you want it to stay that way. Error and status flags will often be hard-coded into your classes. Although they should be publicly and statically
available, client code should not be able to change them.
PHP allows you to define constant properties
within a class. Like global constants, class constants cannot be changed once they are set. A constant property is declared with the const
keyword. Constants are not prefixed with a dollar sign like regular properties. By convention, they are often named using only uppercase characters:
class ShopProduct {
public const AVAILABLE = 0;
public const OUT_OF_STOCK = 1;
}
Constant properties can contain only primitive values. You cannot assign an object to a constant. Like static properties, constant properties are accessed through the class and not an instance. Just as you define a constant without a dollar sign, no leading symbol is required when you refer to one:
print ShopProduct::AVAILABLE;
Note: Support for constant visibility modifiers was introduced in PHP 7.1. They work in just the same way as visibility modifiers do for properties. Attempting to set a value on a constant once it has been declared will cause a parse error.
You should use constants when your property needs to be available across all instances of a class as well as when the property value needs to be fixed and unchanging.
Abstract Classes
An abstract class cannot be instantiated. Instead, it defines (and optionally partially implements) the interface for any class that might extend it.
You define an abstract class with the abstract
keyword. Here I redefine the ShopProductWriter
class I created in the previous chapter, this time as an abstract class:
abstract class ShopProductWriter {
protected array $products = [];
public function addProduct(ShopProduct $shopProduct): void {
$this->products[] = $shopProduct;
}
}
You can create methods and properties as normal, but any attempt to instantiate an abstract object in this way will cause an error:
$writer = new ShopProductWriter();
You can see the error in this output:
Error: Cannot instantiate abstract class popp\ch04\batch03\ShopProductWriter
In most cases, an abstract class will contain at least one abstract method. These are declared once again with the abstract
keyword. An abstract method cannot have an implementation. You declare it in the normal way but end the declaration with a semicolon rather than a method body. Here I add an abstract write()
method to the ShopProductWriter
class:
abstract class ShopProductWriter {
protected array $products = [];
public function addProduct(ShopProduct $shopProduct): void {
$this->products[] = $shopProduct;
}
abstract public function write(): void;
}
In creating an abstract method, you ensure that an implementation will be available in all concrete child classes, but you leave the details of that implementation undefined.
Assume I were to create a class derived from ShopProductWriter
that does not implement the write()
method, as in this example:
class ErroredWriter extends ShopProductWriter {
}
I would face the following error:
Fatal error: Class popp\ch04\batch03\ErroredWriter contains 1 abstract method and must therefore be declared abstract or implement the remaining methods (popp\ch04\batch03\ShopProductWriter::write) in...
So any class that extends an abstract class must implement all abstract methods or itself be declared abstract. An extending class is responsible for more than simply implementing an abstract method. In doing so, it must reproduce the method signature. This means that the access control of the implementing method cannot be stricter than that of the abstract method. The implementing method should also require the same number of arguments as the abstract method, reproducing any class type declarations.
Here are two implementations of ShopProductWriter
—first XmlProductWriter
:
class XmlProductWriter extends ShopProductWriter {
public function write(): void {
$writer = new \XMLWriter();
$writer->openMemory();
$writer->startDocument('1.0', 'UTF-8');
$writer->startElement("products");
foreach ($this->products as $shopProduct) {
$writer->startElement("product");
$writer->writeAttribute("title", $shopProduct->getTitle());
$writer->startElement("summary");
$writer->text($shopProduct->getSummaryLine());
$writer->endElement(); // summary
$writer->endElement(); // product
}
$writer->endElement(); // products
$writer->endDocument();
print $writer->flush();
}
}
This is the more basic TextProductWriter
:
class TextProductWriter extends ShopProductWriter {
public function write(): void {
$str = "PRODUCTS:\n";
foreach ($this->products as $shopProduct) {
$str .= $shopProduct->getSummaryLine() . "\n";
}
print $str;
}
}
So I have created two classes, each with its own implementation of the write()
method. The first outputs XML and the second outputs text. A method that requires a ShopProductWriter
object will not know which of these two classes it is receiving, but it can be absolutely certain that a write()
method is implemented. Note that I don’t test the type of $products
before treating it as an array. This is because this property is both declared an array and initialized in the ShopProductWriter
class.
Interfaces
Although abstract classes let you provide some measure of implementation, interfaces are pure templates. An interface can only define functionality; it can never implement it. An interface is declared with the interface
keyword. It can contain properties and method declarations but not method bodies.
Here’s an interface:
interface Chargeable {
public function getPrice(): float;
}
As you can see, an interface looks very much like a class. Any class that incorporates this interface commits to implementing all the methods it defines or it must be declared abstract.
A class can implement an interface using the implements
keyword in its declaration. Once you have done this, the process of implementing an interface is the same as extending an abstract class that contains only abstract methods. Now I will make the ShopProduct
class implement Chargeable
:
class ShopProduct implements Chargeable {
// ...
protected float $price;
// ...
public function getPrice(): float {
return $this->price;
}
// ...
}
ShopProduct
already had a getPrice()
method, so why might it be useful to implement the Chargeable
interface? Once again, the answer has to do with types. An implementing class takes on the type of the class it extends and the interface that it implements.
This means that the CdProduct
class belongs to the following:
CdProduct
ShopProduct
Chargeable
This can be exploited by client code. To know an object’s type is to know its capabilities. Consider this method:
public function cdInfo(CdProduct $prod): int {
// we know we can call getPlayLength()
$length = $prod->getPlayLength();
// ...
}
The method knows that the $prod
object has a getPlayLength()
method in addition to all the methods defined in the ShopProduct
class and Chargeable
interface.
Passed the same object, however, a method with a more generic type requirement—ShopProduct
rather than CdProduct
—can only know that the provided object contains ShopProduct
methods.
public function addProduct(ShopProduct $prod) {
// even if $prod is a CdProduct object
// we don't *know* this -- so we can't
// presume to use getPlayLength()
// ...
}
Without further testing, the method will know nothing of the getPlayLength()
method.
Passed the same CdProduct
object, a method which required a Chargeable
object knows nothing at all of the ShopProduct
or CdProduct
types:
public function addChargeableItem(Chargeable $item) {
// all we know about $item is that it
// is a Chargeable object -- the fact that it
// is also a CdProduct object is irrelevant.
// We can only be sure of getPrice()
//
//...
}
This method is only concerned with whether the $item
argument contains a getPrice()
method.
Because any class can implement an interface (in fact a class can implement any number of interfaces), interfaces effectively join types that are otherwise unrelated. I might define an entirely new class that implements Chargeable
:
class Shipping implements Chargeable {
public function __construct(private float $price) {
}
public function getPrice(): float {
return $this->price;
}
}
I can pass a Shipping
object to the addChargeableItem()
method just as I can pass it a ShopProduct
object.
The important thing to a client working with a Chargeable
object is that it can call a getPrice()
method. Any other available methods are associated with other types, whether through the object’s own class, a superclass, or another interface. These are irrelevant to the client.
A class can both extend a superclass and implement any number of interfaces. The extends
clause should precede the implements
clause:
class Consultancy extends TimedService implements Bookable, Chargeable {
// ...
}
Notice that the Consultancy
class implements more than one interface. Multiple interfaces follow the implements
keyword in a comma-separated list.
PHP only supports inheritance from a single parent, so the extends
keyword can precede a single class name only.
Traits
As we have seen, interfaces help you manage the fact that like Java, PHP does not support multiple inheritance. In other words, a class in PHP can only extend a single parent. However, you can make a class promise to implement as many interfaces as you like; for each interface it implements, the class takes on the corresponding type.
So interfaces provide types without implementation. But what if you want to share an implementation across inheritance hierarchies? PHP 5.4 introduced traits, and these let you do just that.
A trait is a class-like structure that cannot itself be instantiated but can be incorporated into classes. Any methods defined in a trait become available as part of any class that uses it. A trait changes the structure of a class but doesn’t change its type. Think of traits as includes for classes.
Let’s look at why a trait might be useful.
A Problem for Traits to Solve
Here is a version of the ShopProduct
class with a calculateTax()
method:
class ShopProduct {
private int $taxrate = 20;
// ...
public function calculateTax(float $price): float {
return (($this->taxrate / 100) * $price);
}
}
The calculateTax()
method accepts a $price
argument and calculates a sales tax amount based on the private $taxrate
property.
Of course, a subclass gains access to calculateTax()
. But what about entirely different class hierarchies? Imagine a class named UtilityService
which inherits from another class Service
. If UtilityService
needs to use an identical routine, I might find myself duplicating calculateTax()
in its entirety. Here is Service
:
abstract class Service {
// service oriented stuff
}
And here is UtilityService
:
class UtilityService extends Service {
private int $taxrate = 20;
public function calculateTax(float $price): float {
return (($this->taxrate / 100) * $price);
}
}
Because UtilityService
and ShopProduct
do not share any common base classes, they cannot easily share the calculateTax()
implementation. We are forced, therefore, to copy and paste our implementation from one class to another.
Defining and Using a Trait
One of the core object-oriented design goals I will cover in this book is the removal of duplication. As you will see in Chapter 11, one solution to this kind of duplication is to factor it out into a reusable strategy class. Traits provide another approach—less elegant perhaps, but certainly effective.
Here I declare a single trait that defines a calculateTax()
method and then I include it in both ShopProduct
and UtilityService
:
trait PriceUtilities {
private $taxrate = 20;
public function calculateTax(float $price): float {
return (($this->taxrate / 100) * $price);
}
// other utilities
}
I declare the PriceUtilities
trait with the trait
keyword. The body of a trait looks very similar to that of a class. It is simply a set of methods and properties collected within braces. Once I have declared it, I can access the PriceUtilities
trait from within my classes. I do this with the use
keyword followed by the name of the trait I wish to incorporate. So having declared and implemented the calculateTax()
method in a single place, I go ahead and incorporate it into the ShopProduct
class:
use popp\ch04\batch06_1\PriceUtilities;
class ShopProduct {
use PriceUtilities;
}
Also, of course, I add it to the UtilityService
class:
class UtilityService extends Service {
use PriceUtilities;
}
Now when I invoke these classes, I know that they share the PriceUtilities
implementation without duplication. If I were to find a bug in PriceUtilities
, I could fix it in a single place:
$p = new ShopProduct();
print $p->calculateTax(100) . "\n";
$u = new UtilityService();
print $u->calculateTax(100) . "\n";
Using More Than One Trait
You can include multiple traits in a class by listing each one after the use
keyword, separated by commas. In this example, I define and apply a new trait IdentityTrait
keeping my original PriceUtilities
trait:
trait IdentityTrait {
public function generateId(): string {
return uniqid();
}
}
By applying both PriceUtilities
and IdentityTrait
with the use
keyword, I make the calculateTax()
and the generateId()
methods available to the ShopProduct
class. This means the class offers both the calculateTax()
and generateId()
methods:
class ShopProduct {
use PriceUtilities;
use IdentityTrait;
}
Note: The IdentityTrait
trait provides the generateId()
method. In fact, a database often generates identifiers for objects, but you might switch in a local implementation for testing purposes. You can find out more about objects, databases, and unique identifiers in Chapter 13, which covers the Identity Map pattern. You can learn more about testing and mocking in Chapter 18.
Now I can call both the generateId()
and calculateTax()
methods on a ShopProduct
class:
$p = new ShopProduct();
print $p->calculateTax(100) . "\n";
print $p->generateId() . "\n";
Combining Traits and Interfaces
Although traits are useful, they don’t change the type of the class to which they are applied. So when you apply the IdentityTrait
trait to multiple classes, they won’t share a type that could be hinted for in a method signature.
Luckily, traits play well with interfaces. I can define an interface that requires a generateId()
method and then declare that ShopProduct
implements it:
interface IdentityObject {
public function generateId(): string;
}
If I want ShopProduct
to fulfill the IdentityObject
type, I must now make it implement the IdentityObject
interface:
class ShopProduct implements IdentityObject {
use PriceUtilities;
use IdentityTrait;
}
As before, ShopProduct
uses the IdentityTrait
trait. However, the method this imports, generateId()
, now also fulfills a commitment to the IdentityObject
interface. This means that we can pass ShopProduct
objects to methods and functions that use type hinting to demand IdentityObject
instances like this:
public static function storeIdentityObject(IdentityObject $idobj) {
// do something with the IdentityObject
}
Managing Method Name Conflicts with insteadof
The ability to combine traits is a nice feature, but sooner or later conflicts are inevitable. Consider what would happen, for example, if I were to use two traits that provide a calculateTax()
method:
trait TaxTools {
public function calculateTax(float $price): float {
return 222;
}
}
Because I have included two traits that contain calculateTax()
methods, PHP is unable to work out which should override the other. The result is a fatal error:
Fatal error: Trait method popp\ch04\batch06_3\TaxTools::calculateTax has not been applied as popp\ch04\batch06_3\UtilityService::calculateTax because of collision with popp\ch04\batch06_3\PriceUtilities::calculateTax in...
To fix this problem, I can use the insteadof
keyword. Here’s how:
class UtilityService extends Service {
use PriceUtilities;
use TaxTools {
TaxTools::calculateTax insteadof PriceUtilities;
}
}
In order to apply further directives to a use
statement, I must first add a body. I do this with opening and closing braces. Within this block, I use the insteadof
operator. This requires a fully qualified method reference (i.e., one that identifies both the trait and the method names separated by a scope resolution operator) on the left-hand side. On the right-hand side, insteadof
requires the name of the trait whose equivalent method should be overridden:
TaxTools::calculateTax insteadof PriceUtilities;
The preceding snippet means “Use the calculateTax()
method of TaxTools
instead of the method of the same name in PriceUtilities
.”
So when I run this code:
$u = new UtilityService();
print $u->calculateTax(100) . "\n";
I get the dummy output I planted in TaxTools::calculateTax()
:
222
Aliasing Overridden Trait Methods
We have seen that you can use insteadof
to disambiguate between methods. What do you do, though, if you want to then access the overridden method? The as
operator allows you to alias trait methods. Once again, the as
operator requires a full reference
to a method on its left-hand side. On the right-hand side of the operator, you should put the name of the alias. So here, for example, I reinstate the calculateTax()
method of the PriceUtilities
trait using the new name basicTax()
:
class UtilityService extends Service {
use PriceUtilities;
use TaxTools {
TaxTools::calculateTax insteadof PriceUtilities;
PriceUtilities::calculateTax as basicTax;
}
}
Now the UtilityService
class has acquired two methods: the TaxTools
version of calculateTax()
and the PriceUtilities
version aliased to basicTax()
. Let’s run these methods:
$u = new UtilityService();
print $u->calculateTax(100) . "\n";
print $u->basicTax(100) . "\n";
This gives the following output:
222
20
So PriceUtilities::calculateTax()
has been resurrected as part of the UtilityService
class under the name basicTax()
.
Note: Where a method name clashes between traits, it is not enough to alias one of the method names in the use
block. You must first determine which method supersedes the other using the insteadof
operator. Then you can reassign the discarded method a new name with the as
operator.
Incidentally, you can also use method name aliasing where there is no name clash. You might, for example, want to use a trait method to implement an abstract method signature declared in a parent class or in an interface.
Using Static Methods in Traits
Most of the examples you have seen so far could use static methods because they do not store instance data. There’s nothing complicated about placing a static method in a trait. Here I change the PriceUtilities::$taxrate
property and the PriceUtilities::calculateTax()
methods so that they are static:
trait PriceUtilities {
private static int $taxrate = 20;
public static function calculateTax(float $price): float {
return ((self::$taxrate / 100) * $price);
}
// other utilities
}
Here is UtilityService
back to its minimal form:
class UtilityService extends Service {
use PriceUtilities;
}
All it does is use the PriceUtilities
trait. There is a key difference, though, when it comes to calling the calculateTax()
method:
print UtilityService::calculateTax(100) . "\n";
I must now call the method on the class rather than on an object. As you might expect, this script outputs the following:
20
So static methods are declared in traits and accessed via the host class in the normal way.
Accessing Host Class Properties
You might assume that static methods are really the only way to go as far as traits are concerned. Even trait methods that are not declared static are essentially static in nature, right? Well, wrong. In fact, you can access properties and methods in a host class:
trait PriceUtilities {
public function calculateTax(float $price): float {
// is this good design?
return (($this->taxrate / 100) * $price);
}
// other utilities
}
In the preceding code, I amend the PriceUtilities
trait so that it accesses a property in its host class. Here is a host—PriceUtilities—amended to declare the property:
class UtilityService extends Service {
use PriceUtilities;
public $taxrate = 20;
}
If you think that this is a bad design, you’re right. It’s a spectacularly bad design. Although it’s useful for the trait to access data set by its host class, there is nothing to require the UtilityService
class to actually provide a $taxrate
property. Remember that traits should be usable across many different classes. What is the guarantee or even the likelihood that any host classes will declare a $taxrate
?
On the other hand, it would be great to be able to establish a contract that says essentially “If you use this trait, then you must provide it certain resources.”
In fact, you can achieve exactly this effect. Traits support abstract methods.
Defining Abstract Methods in Traits
You can define abstract methods in a trait in just the same way you would in a class. When a trait is used by a class, it takes on the commitment to implement any abstract methods it declares.
Note: Prior to PHP 8, method signatures for abstract methods defined in traits were not always fully enforced. This meant that in some circumstances, argument and return types might vary in the implementing class from those set down in the abstract method declaration. This loophole has now been shut.
Armed with this knowledge, I can reimplement my previous example so that the trait forces any class that uses it to provide tax rate information:
trait PriceUtilities {
public function calculateTax(float $price): float {
// better design.. we know getTaxRate() is implemented
return (($this->getTaxRate() / 100) * $price);
}
abstract public function getTaxRate(): float;
// other utilities
}
By declaring an abstract getTaxRate()
method in the PriceUtilities
trait, I force the UtilityService
class to provide an implementation:
class UtilityService extends Service {
use PriceUtilities;
public function getTaxRate(): float {
return 20;
}
}
Thanks to the abstract declaration in the trait, if I had not provided a getTaxRate()
method, I would have been rewarded with a fatal error.
Changing Access Rights to Trait Methods
You can, of course, declare a trait method public
, private
, or protected
. However, you can also change this access from within the class that uses the trait. You have already seen that the as
operator can be used to alias a method name. If you use an access modifier on the right-hand side of this operator, it will change the method’s access level rather than its name.
Imagine, for example, you would like to use calculateTax()
from within UtilityService
but not make it available to implementing code. Here’s how you would change the use
statement:
class UtilityService extends Service {
use PriceUtilities {
PriceUtilities::calculateTax as private;
}
public function __construct(private float $price) {
}
public function getTaxRate(): float {
return 20;
}
public function getFinalPrice(): float {
return ($this->price + $this->calculateTax($this->price));
}
}
I deploy the as
operator in conjunction with the private
keyword in order to set private access to calculateTax()
. This means I can access the method from getFinalPrice()
. Here’s an external attempt to access calculateTax()
:
$u = new UtilityService(100);
print $u->calculateTax() . "\n";
Unfortunately, this code will generate an error:
Error: Call to private method popp\ch04\batch06_9\UtilityService::calculateTax() from context ...
Late Static Bindings: The static
Keyword
Now that you’ve seen abstract classes, traits, and interfaces, it’s time to return briefly to static methods. You saw that a static method can be used as a factory, a way of generating instances of the containing class. If you’re as lazy a coder as me, you might chafe at the duplication in an example like this:
abstract class DomainObject {
// ...
}
class User extends DomainObject {
public static function create(): User {
return new User();
}
}
class Document extends DomainObject {
public static function create(): Document {
return new Document();
}
}
I create a superclass named DomainObject
. In a real-world project, of course, this would contain functionality common to its extending classes. Then I create two child classes, User
and Document
. I would like my concrete classes to have static create()
methods.
Note: Why would I use a static factory method when a constructor performs the work of creating an object already? In Chapter 13, I’ll describe a pattern called Identity Map. An Identity Map component generates and manages a new object only if an object with the same distinguishing characteristics is not already under management. If the target object already exists, it is returned. A factory method like create()
would make a good client for a component of this sort.
This code works fine, but it has an annoying amount of duplication. I don’t want to have to create boilerplate code like this for every DomainObject
child class that I create. Instead, I’ll try pushing the create()
method up to the superclass:
abstract class DomainObject {
public static function create(): DomainObject {
return new self();
}
}
Well, that looks neat. I now have common code in one place, and I’ve used self
as a reference to the class. But I have made an assumption about the self
keyword. In fact, it does not act for classes exactly the same way that $this
does for objects. self
does not refer to the calling context; it refers to the context of resolution. So if I run the previous example, I get this:
Error: Cannot instantiate abstract class popp\ch04\batch06\DomainObject
So self
resolves to DomainObject
, the place where `
create()is defined, and not to
Document, the class on which it was called. Until PHP 5.3, this was a serious limitation which spawned many rather clumsy workarounds. PHP 5.3 introduced a concept called late static bindings. The most obvious manifestation of this feature is the keyword:
static.
staticis similar to
selfexcept that it refers to the invoked rather than the containing class. In this case, it means that calling
Document::create()results in a new
Documentobject and not a doomed attempt to instantiate a
DomainObject` object.
So now I can take advantage of my inheritance relationship in a static context:
abstract class DomainObject {
public static function create(): DomainObject {
return new static();
}
}
class User extends DomainObject {
// ...
}
class Document extends DomainObject {
// ...
}
Now if we call create()
on one of the child classes, we should no longer cause an error—and get back an object related to the class we called and not to the class that houses create()
:
print_r(Document::create());
Here is the output:
popp\ch04\batch07\Document Object
(
)
The static
keyword can be used for more than just instantiation. Like self
and parent
, static
can be used as an identifier for static method calls, even from a non-static context. Let’s say I want to include the concept of a group for my DomainObject
classes. By default, in my new classification, all classes fall into category “default,” but I’d like to be able to override this for some branches of my inheritance hierarchy:
abstract class DomainObject {
private string $group;
public function __construct() {
$this->group = static::getGroup();
}
public static function create(): DomainObject {
return new static();
}
public static function getGroup(): string {
return "default";
}
}
class User extends DomainObject {
// ...
}
class Document extends DomainObject {
public static function getGroup(): string {
return "document";
}
}
class SpreadSheet extends Document {
// ...
}
print_r(User::create());
print_r(SpreadSheet::create());
I introduced a constructor to the DomainObject
class. It uses the static
keyword to invoke a static method: getGroup()
. DomainObject
provides the default implementation, but Document
overrides it. I also created a new class SpreadSheet
that extends Document
. Here’s the output:
popp\ch04\batch07\User Object (
[group:popp\ch04\batch07\DomainObject:private] => default
)
popp\ch04\batch07\SpreadSheet Object (
[group:popp\ch04\batch07\DomainObject:private] => document
)
For the User
class, not much clever needs to happen. The DomainObject
constructor calls getGroup()
and finds it locally. In the case of SpreadSheet
, though, the search begins at the invoked class SpreadSheet
itself. It provides no implementation, so the getGroup()
method in the Document
class is invoked. Before PHP 5.3 and late static binding, I would have been stuck with the self
keyword here, which would only look for getGroup()
in the DomainObject
class.
Handling Errors
Things go wrong. Files are misplaced, database servers are left uninitialized, URLs are changed, XML files are mangled, permissions are poorly set, and disk quotas are exceeded. The list goes on and on. In the fight to anticipate every problem, a simple method can sometimes sink under the weight of its own error-handling code.
Here is a simple Conf
class that stores, retrieves, and sets data in an XML configuration file:
class Conf {
private \SimpleXMLElement $xml;
private \SimpleXMLElement $lastmatch;
public function __construct(private string $file) {
$this->xml = simplexml_load_file($file);
}
public function write(): void {
file_put_contents($this->file, $this->xml->asXML());
}
public function get(string $str): ?string {
$matches = $this->xml->xpath("/conf/item[@name=\"$str\"]");
if (count($matches)) {
$this->lastmatch = $matches[0];
return (string)$matches[0];
}
return null;
}
public function set(string $key, string $value): void {
if (! is_null($this->get($key))) {
$this->lastmatch[0] = $value;
return;
}
$conf = $this->xml->conf;
$this->xml->addChild('item', $value)->addAttribute('name', $key);
}
}
The Conf
class uses the SimpleXml
extension to access name-value pairs. Here’s the kind of format with which it is designed to work:
<?xml version="1.0" ?>
<conf>
<item name="user">bob</item>
<item name="pass">newpass</item>
<item name="host">localhost</item>
</conf>
The Conf
class’s constructor accepts a file path, which it passes to simplexml_load_file()
. It stores the resulting SimpleXmlElement
object in a property called $xml
. The get()
method uses XPath to locate an item
element with the given name
attribute, returning its value. set()
either changes the value of an existing item or creates a new one. Finally, the write()
method saves the new configuration data back to the file.
Like much example code, the Conf
class is highly simplified. In particular, it has no strategy for handling nonexistent or unwriteable files. It is also optimistic in outlook. It assumes that the XML document will be well-formed and will contain the expected elements.
Testing for these error conditions is relatively trivial, but I must still decide how to respond to them should they arise. There are generally two options.
First, I could end execution. This is simple but drastic. My humble class would then take responsibility for bringing an entire script crashing down around it. Although methods such as __construct()
and write()
are well placed to detect errors, they do not have the information to decide how to handle them.
Rather than handle the error in my class, then, I could return an error flag of some kind. This could be a Boolean or an integer value such as 0
or -1
. Some classes will also set an error string or flag so that the client code can request more information after a failure.
Many PEAR packages combine these two approaches by returning an error object (an instance of PEAR_Error
) which acts as a notification that an error has occurred and contains the error message within it. This approach is now deprecated, but plenty of classes have not been upgraded, not least because client code often depends on the old behavior.
The problem here is that you pollute your return value. You have to rely on the client coder to test for the return type every time your error-prone method is called. This can be risky. Trust no one!
When you return an error value to calling code, there is no guarantee that the client will be any better equipped than your method to decide how to handle the error. If this is the case, then the problem begins all over again. The client method will have to determine how to respond to the error condition, maybe even implementing a different error-reporting strategy.
Exceptions
PHP 5 introduced exceptions to PHP, a radically different way of handling error conditions. Different for PHP, that is. You will find them hauntingly familiar if you have Java or C++ experience. Exceptions address all of the issues that I have raised so far in this section.
An exception is a special object instantiated from the built-in Exception
class (or from a derived class).
Objects of type Exception
are designed to hold and report error information.
The Exception
class constructor accepts two optional arguments: a message string and an error code. The class provides some useful methods for analyzing error conditions. These are described in Table 4-1.
Table 4-1: The Exception Class’s Public Methods
Method | Description |
---|---|
getMessage() |
Get the message string that was passed to the constructor |
getCode() |
Get the code integer that was passed to the constructor |
getFile() |
Get the file in which the exception was generated |
getLine() |
Get the line number at which the exception was generated |
getPrevious() |
Get a nested Exception object |
getTrace() |
Get a multidimensional array tracing the method calls that led to the exception, including method class file and argument data |
getTraceAsString() |
Get a string version of the data returned by getTrace() |
__toString() |
Called automatically when the Exception object is used in string context. Returns a string describing the exception details |
The Exception
class is fantastically useful for providing error notification and debugging information (the getTrace()
and getTraceAsString()
methods are particularly helpful in this regard). In fact, it is almost identical to the PEAR_Error
class that was discussed earlier. There is much more to an exception than the information it holds,though.
Throwing an Exception
The throw
keyword is used in conjunction with an Exception
object. It halts execution of the current method and passes responsibility for handling the error back to the calling code. Here I amend the __construct()
method to use the throw
statement:
public function __construct(private string $file) {
if (! file_exists($file)) {
throw new \Exception("file '{$file}' does not exist");
}
$this->xml = simplexml_load_file($file);
}
The write()
method can use a similar construct:
public function write(): void {
if (! is_writeable($this->file)) {
throw new \Exception("file '{$this->file}' is not writeable");
}
print "{$this->file} is apparently writeable\n";
file_put_contents($this->file, $this->xml->asXML());
}
Here’s how you might catch these exceptions:
try {
$conf = new Conf("/tmp/conf01.xml");
//$conf = new Conf( "/root/unwriteable.xml" );
//$conf = new Conf( "nonexistent/not_there.xml" );
print "user: " . $conf->get('user') . "\n";
print "host: " . $conf->get('host') . "\n";
$conf->set("pass", "newpass");
$conf->write();
} catch (\Exception $e) {
// handle error in some way
}
As you can see, the catch
block superficially resembles a method declaration. When an exception is thrown, the catch
block in the invoking scope is called. The Exception
object is automatically passed in as the argument variable.
Just as execution is halted within the throwing method when an exception is thrown, so it is within the try
block—control passes directly to the catch
block. There you can perform any error recovery tasks available to you. If you can, avoid falling back on a die
statement. By invoking die
you make testing harder and might prevent other code in your system from performing necessary cleanup operations. If you cannot recover from an error, you can always throw a new exception:
} catch (\Exception $e) {
// handle error in some way
// or
throw new \Exception("Conf error: " . $e->getMessage());
}
Alternatively, you can just rethrow the exception you have been given:
try {
$conf = new Conf("nonexistent/not_there.xml");
} catch (\Exception $e) {
// handle error...
// or rethrow
throw $e;
}
If you have no need of the Exception
object itself in your error handling, you can, as of PHP 8, omit the exception argument altogether and just specify the type:
try {
$conf = new Conf("nonexistent/not_there.xml");
} catch (\Exception) {
// handle error without using the Exception object
}
Subclassing Exception
You can create classes that extend the Exception
class as you would with any user-defined class. There are two reasons why you might want to do this. First, you can extend the class’s functionality. Second, the fact that a derived class defines a new class type can aid error handling in itself.
You can, in fact, define as many catch
blocks as you need for a try
statement. The particular catch
block invoked will depend on the type of the thrown exception and the class type hint in the argument list. Here are some simple classes that extend Exception
:
class XmlException extends \Exception {
public function __construct(private \LibXmlError $error) {
$shortfile = basename($error->file);
$msg = "[{$shortfile} line {$error->line} col {$error->column}] {$error->message}";
$this->error = $error;
parent::__construct($msg, $error->code);
}
public function getLibXmlError(): \LibXmlError {
return $this->error;
}
}
class FileException extends \Exception {
}
class ConfException extends \Exception {
}
The LibXmlError
class is generated behind the scenes when SimpleXml
encounters a broken XML file. It has message
and code
properties, and it resembles the Exception
class. I take advantage of this similarity and use the LibXmlError
object in the XmlException
class. The FileException
and ConfException
classes do nothing more than subclass Exception
. I can now use these classes in my code and amend both __construct()
and write()
:
class Conf {
public function __construct(private string $file) {
if (! file_exists($file)) {
throw new FileException("file '$file' does not exist");
}
$this->xml = simplexml_load_file($file, null, LIBXML_NOERROR);
if (! is_object($this->xml)) {
throw new XmlException(libxml_get_last_error());
}
$matches = $this->xml->xpath("/conf");
if (! count($matches)) {
throw new ConfException("could not find root element: conf");
}
}
public function write(): void {
if (! is_writeable($this->file)) {
throw new FileException("file '{$this->file}' is not writeable");
}
file_put_contents($this->file, $this->xml->asXML());
}
}
__construct()
throws either an XmlException
, a FileException
, or a ConfException
depending on the kind of error it encounters. Note that I pass the option flag LIBXML_NOERROR
to simplexml_load_file()
. This suppresses warnings, leaving me free to handle them with my XmlException
class after the fact. If I encounter a malformed XML file, I know that an error has occurred because simplexml_load_file()
won’t have returned an object. I can then access the error using libxml_get_last_error()
.
The write()
method throws a FileException
if the $file
property points to an unwriteable entity.
So I have established that __construct()
might throw one of three possible exceptions. How can I take advantage of this? Here’s some code that instantiates a Conf
object:
class Runner {
public static function init() {
try {
$conf = new Conf(__DIR__ . "/conf.broken.xml");
print "user: " . $conf->get('user') . "\n";
print "host: " . $conf->get('host') . "\n";
$conf->set("pass", "newpass");
$conf->write();
} catch (FileException $e) {
// permissions issue or non-existent file
throw $e;
} catch (XmlException $e) {
// broken xml
} catch (ConfException $e) {
// wrong kind of XML file
} catch (\Exception $e) {
// backstop: should not be called
}
}
}
I provide a catch
block for each class type. The block invoked depends on the exception type thrown. The first to match will be executed, so remember to place the most generic type at the end and the most specialized at the start. For example, if you were to place the catch
block for Exception
ahead of the block for XmlException
and ConfException
, neither of these would ever be invoked. This is because both of these classes belong to the Exception
type and would therefore match the first test.
The first catch
block (FileException
) is invoked if there is a problem with the configuration file (if the file is nonexistent or unwriteable). The second block (XmlException
) is invoked if an error occurs in parsing the XML file (e.g., if an element is not closed). The third block (ConfException
) is invoked if a valid XML file does not contain the expected root conf
element. The final block (Exception
) should not be reached because my methods only generate the three exceptions which are explicitly handled. It is often a good idea to have a “backstop” block like this in case you add new exceptions to the code during development.
Note: If you do provide a “backstop” catch block, you should ensure that you actually do something about the exception in most instances—failing silently can cause bugs which are hard to diagnose.
The benefit of these fine-grained catch
blocks is that they allow you to apply different recovery or failure mechanisms to different errors. For example, you may decide to end execution, log the error and continue, or explicitly rethrow an error.
Another trick you can play here is to throw a new exception that wraps the current one. This allows you to stake a claim to the error and add your own contextual information while retaining the data encapsulated by the exception you have caught. You can read more about this technique in Chapter 15.
So what happens if an exception is not caught by client code? It is implicitly rethrown and the client’s own calling code is given the opportunity to catch it. This process continues either until the exception is caught or until it can no longer be thrown. At this point, a fatal error occurs. Here’s what would happen if I did not catch one of the exceptions in my example: