Abstraction means describing what something must be able to do while leaving how it does it to the classes that fill in the details.
πA wall socket is an abstraction. It promises "two pins, 230 volts". Your lamp, laptop charger and kettle all plug in without caring whether the power comes from solar panels or a coal plant.
Code against promises, not details
Clear contracts
An interface lists exactly which methods a class must have. Anyone reading it knows what to expect.
Swappable parts
Code that depends on a contract works with any class that keeps the promise: a real mailer, a fake one for tests, a new provider next year.
Shared code without copying
Abstract classes and traits let related or unrelated classes reuse logic instead of duplicating it.
Three tools, three jobs
Interface
A pure promise: method signatures, no code. A class can implement many.
interface Notifier
{
public function send(string $to, string $msg): void;
}
Abstract class
A half-built parent: some real code, some "fill this in" methods. A class extends only one.
abstract class Report
{
abstract protected function rows(): array;
}
Trait
A bundle of methods pasted into any class with use. No "is a" relationship at all.
trait HasSlug
{
public function slug(): string { /* ... */ }
}
From promise to implementation
-
Write an interface
An interface is a list of public method signatures with no bodies. It's a contract: "any class that signs this must have these methods".
<?php interface Notifier { public function send(string $to, string $message): void; } -
Implement it in real classes
A class implements an interface by providing every method it lists. Miss one, or get a type wrong, and PHP refuses to load the class.
class EmailNotifier implements Notifier { public function send(string $to, string $message): void { echo "π§ Emailing {$to}: {$message}" . PHP_EOL; } } class SmsNotifier implements Notifier { public function send(string $to, string $message): void { echo "π± Texting {$to}: {$message}" . PHP_EOL; } } -
Depend on the interface, not the class
Here's the payoff. Type-hint the interface, and your code accepts any class that implements it. It has no idea, and doesn't care, which one it gets.
class CommentAlert { public function __construct(private Notifier $notifier) {} public function newComment(string $author, string $postTitle): void { $this->notifier->send($author, "New comment on {$postTitle}"); } } (new CommentAlert(new EmailNotifier()))->newComment('ada@example.com', 'Hello Laravel'); (new CommentAlert(new SmsNotifier()))->newComment('+15550100', 'Hello Laravel');A class can implement several interfaces at once:
class Post implements JsonSerializable, Countable. -
Share code with an abstract class
An abstract class can't be created with
new. It holds shared code and declares abstract methods that each child must fill in. This "parent runs the recipe, children supply one ingredient" shape is called the template method pattern.abstract class Report { abstract protected function rows(): array; public function toCsv(): string { return implode(PHP_EOL, array_map(fn (array $r) => implode(',', $r), $this->rows())); } } class PopularPostsReport extends Report { protected function rows(): array { return [['Hello Laravel', 1250], ['Arrays 101', 830]]; } } echo (new PopularPostsReport())->toCsv(); new Report(); // Error: Cannot instantiate abstract class Report -
Choose: interface or abstract class?
They overlap, so here's a rule of thumb. You can also combine them: an abstract class can implement an interface and leave some methods for its children.
Interface Abstract class Contains code? No (only signatures and constants) Yes, plus abstract methods Properties? No Yes How many per class? Many ( implements A, B)One ( extends)Use it when⦠Unrelated classes share a capability Related classes share real code -
Reuse code anywhere with traits
A trait is a reusable chunk of methods (and properties) that you "copy in" with
useinside a class. It solves "I want this behaviour in classes that don't share a parent".trait HasSlug { public function slug(): string { return strtolower(trim(preg_replace('/[^a-z0-9]+/i', '-', $this->slugSource()), '-')); } abstract protected function slugSource(): string; } class Post { use HasSlug; public function __construct(public string $title) {} protected function slugSource(): string { return $this->title; } } class Category { use HasSlug; public function __construct(public string $name) {} protected function slugSource(): string { return $this->name; } } echo (new Category('PHP Tips'))->slug(); // php-tipsβ οΈ Traits aren't typesYou can't type-hint a trait (
function x(HasSlug $s)won't work). If code needs to accept "anything with a slug", pair the trait with an interface, such asclass Post implements Sluggable { use HasSlug; }.
Avoid this, do that
public function __construct(private EmailNotifier $notifier) {}
Hard-wired to one concrete class. Switching to SMS, or a fake in tests, means editing this class.
public function __construct(private Notifier $notifier) {}
Depend on the interface, and any implementation can be plugged in from outside.
interface PostManager
{
public function create(); public function publish();
public function email(); public function exportPdf();
public function tweet(); // ...12 more
}
A giant interface forces every implementer to write methods it doesn't need.
interface Publishable { public function publish(): void; }
interface Exportable { public function export(): string; }
Keep interfaces small and focused. Classes implement only the ones that apply.
Good interface names describe what a class can do or is: Notifier, Publishable, Cache. Avoid suffixes like PostInterface unless your team already uses them.
- Write an interface
Shapewitharea(): floatandname(): string. - Implement it in
CircleandRectangleusing promoted constructor properties. - Create an abstract class
Exporterwith a concreteexport(array $shapes): stringthat calls an abstractformat(Shape $shape): string. MakeCsvExporterandJsonExporterchildren. - Write a trait
Timestampswith atouch()method that sets a?DateTimeImmutable $updatedAt, and use it in both shapes.
- Contracts: Laravel's core services are defined as interfaces in
Illuminate\Contracts, such asCache\RepositoryandMail\Mailer. The Service Container decides which class fills each contract. - Traits on models:
use HasFactory, Notifiable, SoftDeletes;mixes features into yourUserandPostmodels. - Marker interfaces:
class SendNewsletter implements ShouldQueuetells Laravel to run a job in the background (see Queues & Jobs). - Abstract bases:
Illuminate\Database\Eloquent\Modelis itself an abstract class you extend.
Key takeaways
- Abstraction separates what must happen from how it happens.
- Interfaces are contracts of method signatures; a class can implement many.
- Abstract classes share real code and force children to fill in abstract methods; they can't be instantiated.
- Type-hint interfaces so implementations can be swapped freely.
- Traits copy reusable methods into unrelated classes with
use, but they aren't types.