PHP has evolved into a feature-rich, widely deployed web development solution. With each new
version released, new features appear, while existing features are improved. PHP's object
support is one such feature that has been improved. Object oriented support first appeared in
PHP3. PHP4 made additional improvements, such as the way constructors are handled. In fact,
the up and coming
Zend Engine 2.0 introduces a new object model, more similar to Java. With PHP's object
support maturing, many of the reasons developers might not take an object oriented approach
are diminishing.
This article is the first in a series of articles that will focus on various object-oriented
design topics. In this article we are going to talk about the concepts surrounding abstract
classes, situations where they are useful in design, how we can implement abstract classes in PHP,
and hopefully some helpful examples to demonstrate the concepts covered.
Prerequisites
In order to get the most out of this article, you need to have a solid understanding of several
OO concepts, such as: what is an object, defining an object, instantiation, inheritance, and
object composition. For an in depth look at the before mentioned topics, check out some of the other
great articles in the
Application Architecture / Object Oriented section on this site. With that being said, let's move on
to our discussion of abstract classes and how you can use them in design.
What Is An Abstract Class?
An abstract class is a base class that is not instantiated. It provides common functionality
to be inherited by subclasses. Another way to think of an abstract class is like a template.
An abstract class contains no code, but instead, defines what code must be defined in subclasses.
Why create a class without any code and one that cannot be instantiated? Well, there are many
situations where abstract classes are quite useful. Let's take a look...
Designing With Abstract Classes
One common use for an abstract class is at the top of an object hierarchy. Perhaps you are designing an
application in which you would prefer all of your objects to share a certain set of common behavior,
such as toString(). Regardless of what each object models within your business domain, you
can reliably call upon this expected behavior, as long as implementation has been provided. This is an
excellent situation where an abstract class will provide us with a definition for common behavior.
An interface definition is another great example of using abstract classes within your application design.
In languages like Java, interfaces are defined with the type interface, and contain public method
signatures exclusively. If you try to put implementation into an interface in Java, you will get a compile
error. PHP does not have an interface type as such. However, you can still get the same feel, by using an
abstract class and keeping out any implementation. With an interface, a certain behavior is defined that
any class can implement in any way it sees fit. Furthermore, these interfaces can be composed within
classes to extend their functionality even further. Later on, we'll take a look at how these interfaces
can be dynamically pluggable at runtime.
Implementing Abstract Classes In PHP
In order to implement an abstract class in PHP, let's review the characteristics of an abstract
class.
- An abstract class cannot be instantiated.
- An abstract class can (and should) contain abstract methods (no implementation).
- If a class contains abstract methods, it must be abstract.
- Inherited classes must implement any inherited abstract methods, or be declared abstract as well.
Currently PHP does not support any of the above mentioned characteristics for classes. So how in
the world are we going to implement an abstract class in PHP? Simple. All we need is a little creativity
and well documented code.
First, let's take a look at how we can prevent the instantiation of a class. For our example, we'll declare
a class named Base, and setup an empty constructor...well, sort of.
<?php
class Base
{
function Base()
{
/* Do not allow instantiation of base class. */
if ( strcasecmp( get_class($this), "Base") == 0 ) {
trigger_error("Instantiation of Base prohibited.", E_USER_ERROR);
return NULL;
}
}
}
?>
Ok, so we added a few extra lines of code to the constructor. Yes, somewhat cheesy and not near as elegant
as adding an abstract modifier to the class declaration (currently not possible in PHP), but it
works. When the constructor is called, we grab the name of the class from the current instance, and compare
it to the name of class Base. If the two names match, we trigger an error halting the script.
Finished? Not quite. Although an attempt to instantiate our base class would trigger an error, nothing
prevents our constructor from being called statically, Base::Base(). Any class, regardless
of inheritance, could call our constructor through static method syntax. In fact, any code practically
anywhere could still call our constructor statically. Since PHP currently doesn't make a distinction
between instance vs. static methods (except for $this at runtime), we just code around it.
<?php
class Base
{
function Base()
{
/* Do not allow instantiation of base class. */
if ( strcasecmp( get_class($this), "Base") == 0 ) {
trigger_error("Instantiation of Base prohibited.", E_USER_ERROR);
return NULL;
}
/* Prevent non subclass from calling constructor. */
if ( !is_subclass_of($this, "Base") ) {
trigger_error("Base constructor call not allowed.", E_USER_ERROR);
return NULL;
}
}
}
?>
We now have an abstract base class that cannot be instantiated or have the constructor called directly,
unless from a subclass. Attempting either of the following calls would result in a script error.
<?php
$base = new Base();
Base::Base();
?>
To finalize this example, let's clean up our code a little. The second if() statement can
actually serve two purposes. It solves our need to prevent the constructor from being called statically.
It indirectly solves the instantiation of the class as well. To see for your self, comment out the first
if() statement, and once more attempt to instantiate the class or call the constructor
statically. You should still get a script error. All that's left is to modify our error message a bit.
The final code provides a simple way to prevent class instantiation and simulate an abstract class.
<?php
class Base
{
function Base()
{
/*
* Do not allow instantiation of base class.
* Prevent non subclass from calling constructor.
*/
if ( !is_subclass_of($this, "Base") ) {
trigger_error("Base instantiation from non subclass.", E_USER_ERROR);
return NULL;
}
}
}
?>
For the remaining characteristics of an abstract class, we will provide well-written comments in our code.
Most of the remaining characteristics (i.e. inherited classes must implement inherited abstract methods)
are forced at compile time by languages such as Java. You can use the method_exists() function
to ensure a subclass has the methods of the base. Of course, this will always return true, since the
methods do exist through inheritance. A distinction between abstract and non-abstract methods doesn't exist
in PHP. There is no sensible way to check for implementation (if anyone knows otherwise, please share your
knowledge). Therefore you must provide appropriate comments informing the implementer what is expected.
Example: Object Hierarchy
We want to provide an abstract class that will live at the top of our object hierarchy. This class will
define core behavior that all classes in our hierarchy should implement. We will call this top-level class
Object. I realize the creativity is somewhat lacking. We have identified 2 core behaviors that we
would like to define in our abstract class: (1) returning a string representation of an object, and
(2) comparing an instance of one object to that of another object. Here is a look at our abstract class.
<?php
class Object
{
function Object()
{
// Enforce abstract behavior.
if ( !is_subclass_of($this, "Object") ) {
trigger_error("Object instantiation from non subclass.", E_USER_ERROR);
return NULL;
}
}
function toString() {}
function equals($object)
{
return ($this === $object);
}
}
?>
Here we have defined an abstract class Object on which we can build our object hierarchy. Let's take
a look at this example in action. Imagine we are designing a human resources application. One obvious class
in our model would be an Employee class. For the OO purists, you could model a Person and
let each person wear an Employee hat through composition, but we'll save that for another discussion.
Let's take a look at our Employee class.
<?php
class Employee extends Object
{
// Member variables.
var $_id;
var $_ssn;
var $_firstName;
var $_lastName;
function Employee($id, $ssn, $firstName, $lastName)
{
// Assign member variables.
$this->_id = $id;
$this->_ssn = $ssn;
$this->_firstName = $firstName;
$this->_lastName = $lastName;
}
function toString()
{
$info = "ID: ".$this->_id."\n";
$info .= "SSN: ".$this->_ssn."\n";
$info .= "FirstName: ".$this->_firstName."\n";
$info .= "LastName: ".$this->_lastName."\n";
return $info;
}
}
?>
Notice our Employee class has provided implementation for the toString() method. No implementation
for the equals() method was provided, since the Object class provides a sufficient one. However,
if the implementation did not suffice, we could have provided our own version of equality checking. As we further
design our object hierarchy based on Object, we know that all our classes will have 2 common behaviors:
equals() and toString().
Example: Interfaces
For our second example, we will see how an abstract class can be used as an interface. We will start out
by designing a few objects that will be a part of a fictitious web application for home-automation (one of
my favorite hobbies). We want to build a web interface to control our various home devices (i.e. lights,
toaster, stereo). However, not all of our devices speak the same language, or protocol. While the X-10 protocol
still dominates the market, we find several other home-automation protocols creeping in. The four major
contenders among home-automation protocols are:
- X-10
- LONworks
- CEBus (EIA IS-60)
- Smart House
The implementation for each of these protocols varies significantly. Yet, amongst these protocols the same
common control functionality exists: (1) turning on a device, (2) turning off a device, and (3) getting the
status of a device. Our interface will provide a template for this common functionality. We'll name our
interface Control. As you can see, the methods do not contain any code.
<?php
class Control
{
function on() {}
function off() {}
function getStatus() {}
}
?>
Earlier I mentioned providing well-written comments in our code. This helps the implementer have a clear
understanding of what behavior is expected. Therefore, we'll update our example with appropriate comments.
<?php
/*
* Class: Control
* Type: Interface
*
* Purpose: A template for common control functionality
* of home automated devices.
*
* Methods: on()
* off()
* getStatus()
*
* Error Codes
* -----------
* 0 - No errors occurred during transmission.
* 1 - An error occurred during transmission.
*
*
* API Developers
* --------------
* Be sure to provide appropriate functionality for each
* method. The application will dynamically instantiate
* the appropriate protocol for a selected device, and will
* expect consistent control functionality.
*/
class Control
{
function Control()
{
/*
* Do not allow instantiation of Control class.
* Prevent non subclass from calling constructor.
*/
if ( !is_subclass_of($this, "Control") ) {
trigger_error("Control instantiation from non subclass.", E_USER_ERROR);
return NULL;
}
}
function on() {} // Turn on a device.
function off() {} // Turn off a device.
function getStatus() {} // Get the status of a device.
}
?>
Now that we have our interface defined, let's implement a protocol. We will work with the X-10 protocol,
since it is the most popular one. We will inherit from Control, and provide the appropriate
implementation in X10_Control.
<?php
class X10_Control extends Control
{
function on()
{
// ...implementation code.
}
function off()
{
// ...implementation code.
}
function getStatus()
{
// ...implementation code.
}
function dim()
{
// ...implementation code.
}
function bright()
{
// ...implementation code.
}
}
?>
Notice the addition of methods dim() and bright(). This functionality is available in the X-10
protocol, but may not be common in other protocols. Therefore, we add it here exclusively. Now that our X-10
protocol class is complete, we want to see how this protocol and others will apply to a home device. We'll start
out by creating a Device class. Let's take a look.
<?php
class Device
{
// Member variables.
var $_type;
var $_protocol;
function Device($deviceID)
{
// Grab device settings from storage.
$deviceRS = DataProvider::getDevice($deviceID);
// Assign settings to members.
$this->_type = $deviceRS->type; // LIGHT, APPLIANCE, AUDIO, etc.
$this->_protocol = $deviceRS->protocol; // "X10"
// Compose pluggable control protocol through dynamic aggregation.
aggregate( $this, $this->_protocol."_Control" );
}
function setProtocol($protocol)
{
// Clear any existing aggregation.
deaggregate( $this, $this->_protocol."_Control" );
// Update protocol.
$this->_protocol = $protocol;
// Assign new aggregation.
aggregate( $this, $this->_protocol."_Control" );
}
}
?>
We have declared two member variables to hold the type of device and protocol used (i.e. X10, CEBus).
In the constructor, we grab the device settings from storage, assign these settings to our instance, and
then apply the appropriate control functionality, based on the protocol specified.
On a side note, PHP supports two types of composition: association and aggregation. The protocol
functionality is composed through aggregation. Now we can swap in and out different protocols by simply
assigning a new protocol type; even at runtime. Today your garage light is X-10 compatible; tomorrow Smart House.
We'll conclude our example by showing how this sort of design can be implemented into our web application.
Imagine we have a screen that lists the various automated devices throughout our house. Each device listing
has a checkbox associated with it. This allows the user to select which device(s) to control. Upon selecting
the device(s), the user can select on one of three commands: ON, OFF, STATUS.
In the processing script, the command is processed for all devices selected. The processing script is
unaware which protocol each devices has implemented. It just knows that the devices have implemented the
Control interface, so it can reliably call on(), off(), or getStatus(). The
following code shows the processing script that is called once the user makes the selection(s) and sends a
control command.
<?php
// Loop through each selected device, processing command.
foreach($_POST["devices"] as $deviceID => $selected) {
$device = Device::getDevice($deviceID);
switch ( $_POST["command"] ) {
case "on":
$device->on();
break;
case "off":
$device->off();
break;
case "getStatus":
$device->getStatus();
}
}
?>
Conclusion
Well that concludes our discussion of abstract classes. By now you should have a good understanding of what
exactly abstract classes are, as well as the knowledge to put them to use. Be sure to study the design of
various PHP applications that are available on the 'net. You'd be surprised how many developers are already
putting them to use in PHP today.
The code presented in this article is available for download. Take a look at each example to see how these
concepts come together. If this article helped, let me know. Until next time... [editor's note: there was a little confusion regarding the sample code. I've asked Jonathan to resend it and will post it here ASAP. 20030207 - JS]