Homework: Extend Your Previous Class with Inheritance
Learning Objectives
- Master PHP programming concepts
- Write clean, maintainable code
- Apply best practices
- Build dynamic applications
Understanding the Problem
In this homework assignment, we'll apply George Polya's problem-solving method to extend a class using inheritance. This builds on our previous lessons where we've created basic classes, and now we're taking the next step to leverage the power of inheritance in PHP.
What is Inheritance?
Inheritance allows a class (the child or derived class) to inherit properties and methods from another class (the parent or base class). Think of it like genetic inheritance: a child inherits traits from their parents, but can also have their own unique traits.
Assignment Requirements
We need to extend a previously created class using inheritance. This typically involves:
- Creating a parent class (which we've already done in previous homework)
- Creating one or more child classes that extend the parent
- Adding new properties and methods to the child classes
- Overriding parent methods when necessary
- Demonstrating polymorphism (using parent and child objects interchangeably)
For this assignment, let's assume we previously created a Product class. We'll now extend it to create specialized product types.
Devising a Plan
Let's break down our approach for extending the Product class with inheritance:
- Review the base
Productclass from our previous homework - Identify what specialized product types we could create (e.g.,
PhysicalProduct,DigitalProduct) - Determine what unique properties and methods each specialized product type should have
- Implement inheritance by extending the
Productclass - Add constructors in child classes, calling the parent constructor
- Override parent methods where needed to provide specialized behavior
- Create a test script to demonstrate the inheritance in action
Whiteboard Plan
Here's our simplified plan:
- Create folder structure:
product_inheritance/(main folder)product_inheritance/classes/(for all class files)product_inheritance/index.php(test script)
- Define base
Productclass inclasses/Product.php - Create
PhysicalProductclass inclasses/PhysicalProduct.php- Add properties: weight, dimensions, shipping cost
- Override methods as needed
- Create
DigitalProductclass inclasses/DigitalProduct.php- Add properties: download URL, file size
- Override methods as needed
- Create test script in
index.phpto instantiate and test all classes
Implementing the Solution
Step 1: Create File Structure
First, let's create our folder structure:
product_inheritance/
├── classes/
│ ├── Product.php
│ ├── PhysicalProduct.php
│ └── DigitalProduct.php
└── index.php
Step 2: Define the Base Product Class
Let's start by defining our base Product class from the previous homework:
File: classes/Product.php
<?php
/**
* Base Product class
*
* This class represents a generic product with basic properties and methods.
*/
class Product {
// Protected properties (accessible to child classes)
protected $name;
protected $price;
protected $description;
/**
* Constructor
*
* @param string $name The name of the product
* @param float $price The price of the product
* @param string $description The product description
*/
public function __construct($name, $price, $description = '') {
$this->name = $name;
$this->price = $price;
$this->description = $description;
}
/**
* Get the product name
*
* @return string
*/
public function getName() {
return $this->name;
}
/**
* Get the product price
*
* @return float
*/
public function getPrice() {
return $this->price;
}
/**
* Get the product description
*
* @return string
*/
public function getDescription() {
return $this->description;
}
/**
* Get product details as an array
*
* @return array
*/
public function getDetails() {
return [
'name' => $this->name,
'price' => $this->price,
'description' => $this->description
];
}
/**
* Calculate product tax
*
* @param float $rate The tax rate as a decimal (e.g., 0.1 for 10%)
* @return float The calculated tax amount
*/
public function calculateTax($rate = 0.1) {
return $this->price * $rate;
}
}
Step 3: Create the PhysicalProduct Class
Now, let's create our first child class for physical products:
File: classes/PhysicalProduct.php
<?php
// Include the parent class
require_once 'Product.php';
/**
* PhysicalProduct class
*
* This class extends the base Product class to represent
* physical products with weight, dimensions, and shipping costs.
*/
class PhysicalProduct extends Product {
// Additional properties specific to physical products
protected $weight;
protected $dimensions;
/**
* Constructor
*
* @param string $name The name of the product
* @param float $price The price of the product
* @param string $description The product description
* @param float $weight The weight in kilograms
* @param array $dimensions The dimensions as [length, width, height]
*/
public function __construct($name, $price, $description = '', $weight = 0, $dimensions = []) {
// Call the parent constructor first
parent::__construct($name, $price, $description);
// Initialize our own properties
$this->weight = $weight;
$this->dimensions = $dimensions;
}
/**
* Get the product weight
*
* @return float
*/
public function getWeight() {
return $this->weight;
}
/**
* Get the product dimensions
*
* @return array
*/
public function getDimensions() {
return $this->dimensions;
}
/**
* Calculate shipping cost based on weight and distance
*
* @param float $distance The shipping distance in miles
* @return float The calculated shipping cost
*/
public function calculateShippingCost($distance) {
// Basic formula: $5 base cost + $2 per kg + $0.10 per mile
return 5 + ($this->weight * 2) + ($distance * 0.1);
}
/**
* Get detailed product information
*
* Override the parent method to include physical product details
*
* @return array
*/
public function getDetails() {
// Get the basic details from the parent method
$details = parent::getDetails();
// Add our specific details
$details['weight'] = $this->weight;
$details['dimensions'] = $this->dimensions;
$details['type'] = 'Physical Product';
return $details;
}
/**
* Calculate tax for physical products
*
* May have different tax rules than other product types
*
* @param float $rate The tax rate as a decimal
* @return float The calculated tax amount
*/
public function calculateTax($rate = 0.1) {
// For example, physical products might have a different tax calculation
// Here we're applying the tax rate to the price as in the parent class
return parent::calculateTax($rate);
// In a real application, you might have different rules, like:
// return $this->price * $rate * (1 - $this->getDiscountMultiplier());
}
}
Step 4: Create the DigitalProduct Class
Now, let's create our second child class for digital products:
File: classes/DigitalProduct.php
<?php
// Include the parent class
require_once 'Product.php';
/**
* DigitalProduct class
*
* This class extends the base Product class to represent
* digital products with download URL and file size.
*/
class DigitalProduct extends Product {
// Additional properties specific to digital products
protected $downloadUrl;
protected $fileSize; // In MB
/**
* Constructor
*
* @param string $name The name of the product
* @param float $price The price of the product
* @param string $description The product description
* @param string $downloadUrl The product download URL
* @param float $fileSize The file size in MB
*/
public function __construct($name, $price, $description = '', $downloadUrl = '', $fileSize = 0) {
// Call the parent constructor first
parent::__construct($name, $price, $description);
// Initialize our own properties
$this->downloadUrl = $downloadUrl;
$this->fileSize = $fileSize;
}
/**
* Get the product download URL
*
* @return string
*/
public function getDownloadUrl() {
return $this->downloadUrl;
}
/**
* Get the product file size
*
* @return float
*/
public function getFileSize() {
return $this->fileSize;
}
/**
* Generate a time-limited download link
*
* @param int $validForHours Number of hours the link is valid
* @return string The generated download link
*/
public function generateDownloadLink($validForHours = 24) {
// Generate a temporary token
$token = md5(uniqid(rand(), true));
// Create an expiry timestamp
$expires = time() + ($validForHours * 3600);
// In a real app, you'd store this token and expiry in a database
// Return a URL with the token and expiry
return $this->downloadUrl . '?token=' . $token . '&expires=' . $expires;
}
/**
* Get detailed product information
*
* Override the parent method to include digital product details
*
* @return array
*/
public function getDetails() {
// Get the basic details from the parent method
$details = parent::getDetails();
// Add our specific details
$details['download_url'] = $this->downloadUrl;
$details['file_size'] = $this->fileSize;
$details['type'] = 'Digital Product';
return $details;
}
/**
* Calculate tax for digital products
*
* Digital products often have different tax rates
*
* @param float $rate The tax rate as a decimal
* @return float The calculated tax amount
*/
public function calculateTax($rate = 0.05) {
// Digital products often have reduced tax rates
// Note we're using 0.05 (5%) as the default instead of 0.1 (10%)
return $this->price * $rate;
}
}
Step 5: Create a Test Script
Finally, let's create a test script to demonstrate our inheritance hierarchy:
File: index.php
<?php
// Include our class files
require_once 'classes/Product.php';
require_once 'classes/PhysicalProduct.php';
require_once 'classes/DigitalProduct.php';
// Create an instance of each class
$product = new Product('Generic Product', 19.99, 'A standard product');
$physicalProduct = new PhysicalProduct('Coffee Mug', 12.99, 'A ceramic coffee mug', 0.5, [10, 8, 12]);
$digitalProduct = new DigitalProduct('E-book: PHP Mastery', 24.99, 'Learn PHP from beginner to advanced', 'https://example.com/downloads/php-mastery.pdf', 15.5);
// Display product information
echo '<h2>Base Product:</h2>';
echo 'Name: ' . $product->getName() . '<br>';
echo 'Price: $' . number_format($product->getPrice(), 2) . '<br>';
echo 'Description: ' . $product->getDescription() . '<br>';
echo 'Tax (10%): $' . number_format($product->calculateTax(), 2) . '<br>';
echo '<h2>Physical Product:</h2>';
echo 'Name: ' . $physicalProduct->getName() . '<br>';
echo 'Price: $' . number_format($physicalProduct->getPrice(), 2) . '<br>';
echo 'Description: ' . $physicalProduct->getDescription() . '<br>';
echo 'Weight: ' . $physicalProduct->getWeight() . ' kg<br>';
echo 'Dimensions: ' . implode(' x ', $physicalProduct->getDimensions()) . ' cm<br>';
echo 'Tax (10%): $' . number_format($physicalProduct->calculateTax(), 2) . '<br>';
echo 'Shipping Cost (100 miles): $' . number_format($physicalProduct->calculateShippingCost(100), 2) . '<br>';
echo '<h2>Digital Product:</h2>';
echo 'Name: ' . $digitalProduct->getName() . '<br>';
echo 'Price: $' . number_format($digitalProduct->getPrice(), 2) . '<br>';
echo 'Description: ' . $digitalProduct->getDescription() . '<br>';
echo 'File Size: ' . $digitalProduct->getFileSize() . ' MB<br>';
echo 'Download URL: ' . $digitalProduct->getDownloadUrl() . '<br>';
echo 'Tax (5%): $' . number_format($digitalProduct->calculateTax(), 2) . '<br>';
echo 'Download Link: ' . $digitalProduct->generateDownloadLink() . '<br>';
// Demonstrate polymorphism by processing an array of different product types
echo '<h2>Product Details (Demonstrating Polymorphism):</h2>';
$products = [$product, $physicalProduct, $digitalProduct];
foreach ($products as $index => $prod) {
echo '<h3>Product ' . ($index + 1) . ':</h3>';
echo '<pre>';
print_r($prod->getDetails());
echo '</pre>';
}
Looking Back and Reflecting
Key Concepts Applied
- Inheritance: We created specialized product classes that inherit from a base class
- Parent Constructor Calling: Child classes call the parent constructor using
parent::__construct() - Method Overriding: Child classes override methods like
getDetails()andcalculateTax()to provide specialized behavior - Polymorphism: We demonstrated how objects of different classes can be treated uniformly (through the
getDetails()method) - Code Reuse: Child classes reuse code from the parent class, avoiding duplication
Expected Output
When you run index.php, you should see output similar to:
Base Product:
Name: Generic Product
Price: $19.99
Description: A standard product
Tax (10%): $2.00
Physical Product:
Name: Coffee Mug
Price: $12.99
Description: A ceramic coffee mug
Weight: 0.5 kg
Dimensions: 10 x 8 x 12 cm
Tax (10%): $1.30
Shipping Cost (100 miles): $16.00
Digital Product:
Name: E-book: PHP Mastery
Price: $24.99
Description: Learn PHP from beginner to advanced
File Size: 15.5 MB
Download URL: https://example.com/downloads/php-mastery.pdf
Tax (5%): $1.25
Download Link: https://example.com/downloads/php-mastery.pdf?token=[random_token]&expires=[timestamp]
Product Details (Demonstrating Polymorphism):
Product 1:
Array
(
[name] => Generic Product
[price] => 19.99
[description] => A standard product
)
Product 2:
Array
(
[name] => Coffee Mug
[price] => 12.99
[description] => A ceramic coffee mug
[weight] => 0.5
[dimensions] => Array
(
[0] => 10
[1] => 8
[2] => 12
)
[type] => Physical Product
)
Product 3:
Array
(
[name] => E-book: PHP Mastery
[price] => 24.99
[description] => Learn PHP from beginner to advanced
[download_url] => https://example.com/downloads/php-mastery.pdf
[file_size] => 15.5
[type] => Digital Product
)
Taking It Further
Further Extensions and Improvements
Here are some ways you could extend this solution:
Create a SubscriptionProduct Class
For products with recurring billing, you could create a SubscriptionProduct class:
File: classes/SubscriptionProduct.php
<?php
require_once 'Product.php';
class SubscriptionProduct extends Product {
protected $billingCycle; // 'monthly', 'yearly', etc.
protected $billingPeriods; // number of billing periods
public function __construct($name, $price, $description = '', $billingCycle = 'monthly', $billingPeriods = 12) {
parent::__construct($name, $price, $description);
$this->billingCycle = $billingCycle;
$this->billingPeriods = $billingPeriods;
}
public function getBillingCycle() {
return $this->billingCycle;
}
public function getBillingPeriods() {
return $this->billingPeriods;
}
public function getTotalPrice() {
return $this->price * $this->billingPeriods;
}
public function getDetails() {
$details = parent::getDetails();
$details['billing_cycle'] = $this->billingCycle;
$details['billing_periods'] = $this->billingPeriods;
$details['total_price'] = $this->getTotalPrice();
$details['type'] = 'Subscription Product';
return $details;
}
public function calculateTax($rate = 0.1) {
// Calculate tax on the recurring price
return $this->price * $rate;
}
}
Implement a ProductFactory
A factory class can create different product types based on input:
File: classes/ProductFactory.php
<?php
require_once 'Product.php';
require_once 'PhysicalProduct.php';
require_once 'DigitalProduct.php';
require_once 'SubscriptionProduct.php';
class ProductFactory {
/**
* Create a product of the specified type
*
* @param string $type The product type ('physical', 'digital', 'subscription')
* @param array $data The product data
* @return Product A product instance
*/
public static function createProduct($type, $data) {
switch (strtolower($type)) {
case 'physical':
return new PhysicalProduct(
$data['name'] ?? '',
$data['price'] ?? 0,
$data['description'] ?? '',
$data['weight'] ?? 0,
$data['dimensions'] ?? []
);
case 'digital':
return new DigitalProduct(
$data['name'] ?? '',
$data['price'] ?? 0,
$data['description'] ?? '',
$data['download_url'] ?? '',
$data['file_size'] ?? 0
);
case 'subscription':
return new SubscriptionProduct(
$data['name'] ?? '',
$data['price'] ?? 0,
$data['description'] ?? '',
$data['billing_cycle'] ?? 'monthly',
$data['billing_periods'] ?? 12
);
default:
return new Product(
$data['name'] ?? '',
$data['price'] ?? 0,
$data['description'] ?? ''
);
}
}
}
Add Abstract Methods and Properties
You could convert the Product class to an abstract class, forcing child classes to implement certain methods:
abstract class Product {
// Properties...
// Regular methods...
/**
* Get the product type
*
* @return string
*/
abstract public function getType();
/**
* Get product-specific details
*
* @return array
*/
abstract protected function getSpecificDetails();
/**
* Get full product details
*
* @return array
*/
public function getDetails() {
$details = [
'name' => $this->name,
'price' => $this->price,
'description' => $this->description,
'type' => $this->getType()
];
// Merge with product-specific details
return array_merge($details, $this->getSpecificDetails());
}
}
Real-World Applications
This product class hierarchy has many real-world applications:
- E-commerce Systems: Online stores need to handle various product types differently
- Inventory Management: Physical products require stock tracking, while digital products don't
- Order Processing: Physical products need shipping, digital products need download links
- Tax Calculations: Different product types often have different tax rules
- Content Management Systems: WordPress and similar systems use inheritance for different content types
WordPress Application
In WordPress development, inheritance is used extensively:
- WooCommerce Products: WooCommerce has a base product class with specialized child classes for simple, variable, grouped, and external products
- Custom Post Types: You can create specialized post types that inherit from WordPress's base post functionality
- Widget Development: Custom widgets extend the
WP_Widgetclass - Plugin Development: Many plugins use inheritance to create extensible frameworks
Here's a simplified example of a WooCommerce-style product class hierarchy:
Common Mistakes and How to Avoid Them
Forgetting to Call the Parent Constructor
One of the most common mistakes when using inheritance is forgetting to call the parent constructor.
// Incorrect
class ChildClass extends ParentClass {
public function __construct($param1, $param2, $param3) {
// Missing parent constructor call
$this->param3 = $param3;
}
}
// Correct
class ChildClass extends ParentClass {
public function __construct($param1, $param2, $param3) {
// Call parent constructor first
parent::__construct($param1, $param2);
$this->param3 = $param3;
}
}
Using Private Instead of Protected
Another common mistake is using private for properties and methods in the parent class that should be accessible to child classes.
// Problematic
class ParentClass {
private $value; // Child classes cannot access this
private function helper() { // Child classes cannot use this
// ...
}
}
// Better
class ParentClass {
protected $value; // Child classes can access this
protected function helper() { // Child classes can use this
// ...
}
}
Creating Too Deep Inheritance Hierarchies
Inheritance hierarchies that are too deep can become hard to understand and maintain.
// Too deep (harder to understand and maintain)
class Vehicle { /* ... */ }
class MotorVehicle extends Vehicle { /* ... */ }
class Car extends MotorVehicle { /* ... */ }
class SportsCar extends Car { /* ... */ }
class ConvertibleSportsCar extends SportsCar { /* ... */ }
// Better approach: Flatter hierarchy + composition
class Vehicle { /* ... */ }
class Car extends Vehicle {
protected $engine; // Composition
protected $transmission; // Composition
public function __construct($engine, $transmission) {
$this->engine = $engine;
$this->transmission = $transmission;
}
}
Overriding Methods Without Understanding Parent Implementation
Be careful when overriding methods without understanding what the parent method does.
// Problematic
class ParentClass {
public function process() {
$this->validate();
$this->save();
$this->notify();
}
protected function validate() { /* ... */ }
protected function save() { /* ... */ }
protected function notify() { /* ... */ }
}
class ChildClass extends ParentClass {
// Completely replacing parent method, losing its functionality
public function process() {
// Only doing saving, losing validation and notification
$this->save();
}
}
// Better
class ChildClass extends ParentClass {
// Extending parent method
public function process() {
// Call parent method to keep its functionality
parent::process();
// Add additional functionality
$this->additionalProcessing();
}
protected function additionalProcessing() {
// ...
}
}
Additional Exercises
Exercise 1: Create a Book Product Hierarchy
Create a class hierarchy for different types of books:
- Create a base
Bookclass with properties for title, author, and price - Create child classes for
PhysicalBook(with properties for weight, dimensions) andEBook(with properties for file format, download URL) - Add appropriate methods for each class
- Create a test script to demonstrate the hierarchy
Exercise 2: Implement an Abstract Base Class
Modify the product hierarchy to use an abstract base class:
- Make the
Productclass abstract - Add at least one abstract method that all child classes must implement
- Update the child classes to implement the abstract method(s)
- Update the test script to demonstrate the new structure
Exercise 3: Add Interface Implementation
Extend the product hierarchy to include interfaces:
- Create a
Shippableinterface for products that can be shipped - Create a
Downloadableinterface for products that can be downloaded - Update the appropriate product classes to implement these interfaces
- Create a test script that demonstrates polymorphism using the interfaces