Try each exercise first, then compare with the solution.
Exercise 1: shapes with an abstract class
Create an abstract class Shape with an abstract method area() and a concrete method label() that returns "<ClassName>: <area>" (area rounded to 1 decimal). Add Square and Triangle (base and height). Print the label of each in a loop.
Show a possible solution
<?php
abstract class Shape
{
abstract public function area(): float;
public function label(): string
{
return static::class . ": " . round($this->area(), 1);
}
}
class Square extends Shape
{
public function __construct(private float $side) {}
public function area(): float
{
return $this->side ** 2;
}
}
class Triangle extends Shape
{
public function __construct(private float $base, private float $height) {}
public function area(): float
{
return 0.5 * $this->base * $this->height;
}
}
foreach ([new Square(3), new Triangle(4, 5)] as $shape) {
echo $shape->label() . "\n";
}
Square: 9
Triangle: 10
Exercise 2: an interface for discounts
Define an interface Discount with apply(float $price): float. Implement PercentDiscount and FixedDiscount. Write a function finalPrice(float $price, Discount ...$discounts) that applies all of them in order.
Show a possible solution
<?php
interface Discount
{
public function apply(float $price): float;
}
class PercentDiscount implements Discount
{
public function __construct(private float $percent) {}
public function apply(float $price): float
{
return $price * (1 - $this->percent / 100);
}
}
class FixedDiscount implements Discount
{
public function __construct(private float $amount) {}
public function apply(float $price): float
{
return max(0, $price - $this->amount);
}
}
function finalPrice(float $price, Discount ...$discounts): float
{
foreach ($discounts as $discount) {
$price = $discount->apply($price);
}
return $price;
}
echo finalPrice(1000, new PercentDiscount(10), new FixedDiscount(50));
850
Exercise 3: a trait for slugs
Write a trait HasSlug with a method slug() that turns the object’s title property into a lowercase, hyphenated slug. Use it in an Article class.
Show a possible solution
<?php
trait HasSlug
{
public function slug(): string
{
$slug = strtolower(trim($this->title));
$slug = preg_replace('/[^a-z0-9]+/', "-", $slug);
return trim($slug, "-");
}
}
class Article
{
use HasSlug;
public function __construct(public string $title) {}
}
echo (new Article("PHP: Traits & Slugs!"))->slug();
php-traits-slugs
Exercise 4: inject a logger
Create a Logger interface and an ArrayLogger that stores messages in an array. Write a Signup class that receives a Logger in its constructor and logs "Signed up: <email>". Show the stored messages.
Show a possible solution
<?php
interface Logger
{
public function log(string $message): void;
}
class ArrayLogger implements Logger
{
public array $messages = [];
public function log(string $message): void
{
$this->messages[] = $message;
}
}
class Signup
{
public function __construct(private Logger $logger) {}
public function register(string $email): void
{
$this->logger->log("Signed up: $email");
}
}
$logger = new ArrayLogger();
$signup = new Signup($logger);
$signup->register("ana@example.com");
$signup->register("ben@example.com");
echo implode("\n", $logger->messages);
Signed up: ana@example.com
Signed up: ben@example.com
Exercise 5: choose composition
A Report can be exported as CSV or as JSON. Without inheritance, design it so that you can add a new format without changing Report.
Show a possible solution
<?php
interface Exporter
{
public function export(array $rows): string;
}
class JsonExporter implements Exporter
{
public function export(array $rows): string
{
return json_encode($rows);
}
}
class CsvExporter implements Exporter
{
public function export(array $rows): string
{
return implode("\n", array_map(fn($r) => implode(",", $r), $rows));
}
}
class Report
{
public function __construct(private array $rows, private Exporter $exporter) {}
public function render(): string
{
return $this->exporter->export($this->rows);
}
}
$rows = [["Pen", 12], ["Book", 250]];
echo (new Report($rows, new JsonExporter()))->render() . "\n";
echo (new Report($rows, new CsvExporter()))->render();
[["Pen",12],["Book",250]]
Pen,12
Book,250