Implementing Abstract Classes and Interfaces with Traits

Spread the news

There are some very helpful ways to organize classes and objects in PHP. Interfaces, Traits, and Abstract Classes can work together to make code that follows the rules of abstraction. Using these OOP features in PHP allows us to create code that is easy to extend and maintain, saving time and money down the road.  Let’s look at what these structures are as well as how and when abstract classes, traits, and interfaces can be useful in Object Oriented PHP.

Classes

The first and most common type of object is the generic class.  Using the keyword class creates a new type of object that can later be instantiated into a variable in the application.  In its simplest form the code class MyObject { } would define a new object type called MyObject and leave it completely empty. Creating a new object that is of the type MyObject is as easy as $obj = new MyObject(); One of the most important things about classes is that they can extend other classes, they can implement interfaces, and they can use traits.

Interfaces

Interfaces are basically maps of what public methods can be called. They can also define constants, but not properties. If an object implements an interface, then that class is basically guaranteeing, or making a contract, to implement all the necessary public methods that the interface calls for. Both regular classes and abstract classes can implement interfaces, and these could be helpful in your application to create decoupled, beautiful code, while also keeping future developers on your team happy with you. For example, we could create an interface that is a report…  the interface can have a method to generate filters, generate pagination, and run the report.  Then, we could implement this report interface in all of your reports across the system – regardless of what kind of report it is.  Any report that is created in the future should follow this same structure so that the reporting controller can work with any report type.

I’m not saying that we need to use interfaces for reporting in all applications – that would depend on the application’s needs. Report as an Interface is just an example. Interfaces can be used anywhere in the application to ascribe how a class will interact with components, while leaving implementation of how to do the actual work up to the class implementing it.  For example, we could have some reports that run queries on the database, other reports that read CSV files from an S3 bucket in AWS, and others that parse log files to generate their respective output.  The most important thing there is that the system can take any report and always knows exactly what to do with the object.  Need a new report – no problem! The developer can look at the interface and immediately knows what the controller is going to be expecting.

Interfaces are created with the interface keyword as such:

interface Report {
	public function setFilters(Array $filters);
	public function setPaginationProperties(Int $resultsPerPage);
	public function runReport(Int $page=0);
}

Traits

Traits, on the other hand, are exactly the opposite – they are code that has no interface.  A trait is a set of methods that we will want to include in multiple classes. This can be helpful as it allows us to maintain a single file with a trait, even though the code in it is used in multiple other classes across the system.  For example, in our reporting scheme, a trait could be used to format the code for output in multiple formats.  Maybe we need to be able to get the report in JSON, CSV, XLS, and HTML.  It’s generally always bad to put copied code in multiple places (if we ever then wanted to change the code, we would have to change it in every instance), so a trait might be the best way to go.

Traits are created using the trait keyword. They look in every way like a class except that they, like interfaces and abstract classes, cannot be instantiated directly. A basic trait would be created like this:

trait formatReportArrayForOutput {
	function formatReportArrayAsCSV(Array $array):string {
		//code to convert array to csv
		return '';
	}
	function formatReportArrayAsHTML(Array $array):string {
		//code to convert array to HTML
		return '';
	}
}

This creates a trait called formatReportArrayForOutput that brings the two methods contained therein into any class that “uses” it, which is done with the use keyword inside a class: class testClass { use formatReportArrayForOutput; }.

Abstract Classes

This brings us to abstract classes.  This type of class sits somewhere between a regular class and an interface in that it can have its own methods/properties but also some “abstract” methods which need to be implemented by the extending class. If we were to use reporting structures as an example with abstract classes, we could use them to add in some common “report” functions that are just for this type of class. Meanwhile, other functions that can  defined as abstract methods, and not included in the abstract class. These abstract classes must be implemented in the functions that extend them.

Abstract classes cannot be instantiated directly – they must always be extended by a regular class.  Here’s a basic example of an abstract class:

abstract class SQLReport implements Report {
	use formatReportArrayForOutput;

	protected $resultsPerPage = -1;
	protected $filters = [];

	abstract function runReport(Int $page=0);

	public function setFilters(Array $filters) {
		$this->filters = $filters;
	}
	public function setPaginationProperties(Int $resultsPerPage) {
		$this->resultsPerPage = $resultsPerPage;
	}
}

This is a very simple example. As you can see, the classSQLReport has the implementation for the setFilters and setPaginationProperties required by the interface Report, but it just defines runReport as an abstract function which needs to be implemented in any class that extends it. In all reality, if a class tries to extend an abstract class but does not implement all the abstract functions listed in the abstract class, PHP will throw a Fatal Error: PHP Fatal error: Class Test contains 1 abstract method and must therefore be declared abstract or implement the remaining methods.

The Final Class

Bringing this all together in the end we can create a final class that extends SQLReport and contains real logic for the report itself. If we were creating just the one report, or even one type of report, all this stuff with interfaces, traits, and abstract classes would be a complete waste of time, but if we are making multiple different reports, these object features can save a lot of time, while also making the code easier to maintain. With all the code above in play, we can easily create various reports by extending the abstract class(es) and just focusing on the actual report logic:

class OrderReport extends SQLReport {
	function runReport(Int $page=0) {
		// TODO: Implement runReport() method.
		return $this->formatReportArrayAsHTML($dataArray);
	}
}
class InventoryReport extends SQLReport {
	function runReport(Int $page=0) {
		// TODO: Implement runReport() method.
		return $this->formatReportArrayAsHTML($dataArray);
	}
}
class UserReport extends SQLReport {
	function runReport(Int $page=0) {
		// TODO: Implement runReport() method.
		return $this->formatReportArrayAsHTML($dataArray);
	}
}

At this point we have a completely decoupled reporting system that can be called very easily from a controller, for example:

class ReportController {
   public function GenerateReportHTML(Report $report){
      $report->setPaginationProperties(-1);
      $report->setFilters([]);//no filter
      print $report->runReport();
   }
}

Summary

Interfaces, Traits, and Abstract classes are tools to help push our code into more readable, extendable, decoupled concepts.

  • Interfaces contain a map of what public functions are needed for a type of class, with the idea that multiple classes would be created and should comply with this list of methods so other components know what to expect.
  • Traits contain methods that are decoupled and can be imported into other classes with the use keyword.
  • Abstract classes allow for some planning features by having abstract methods that must be implemented by another class, similar to how an interface defines the method mapping, but can also contain some methods that will be reused in multiple later extending classes.

These three OOP features make adding features as well as maintaining existing ones easier.  Interfaces are primarily used to keep it easy to add new features, while traits and abstract classes keep similar code together and hand it out through inheritance, making it easier to maintain. Combining them properly will make a large project easier to understand and modify, saving time and money down the road.

Going Deeper Still…

If you are interested in reading more on this topic, I highly recommend PHP Objects, Patterns, and Practice by Matt Zandstra.  The Fifth Edition, has been updated for all the changes in PHP7 and includes detailed information on interfaces and traits.  Zandstra does a great job at explaining the concepts in a very thorough way. You won’t be disappointed with the value in this book for advanced OOP in PHP!


Spread the news

2 Comments

  1. Carlos

    Thanks very much for this article. Referred here from PHPWeekly newsletter. Couple questions..

    > If we were creating just the one report, or even one type of report, all this stuff with interfaces, traits, and abstract classes would be a complete waste of time, but if we are making multiple different reports, these object features can save a lot of time, while also making the code easier to maintain.

    1. I believe ‘all this stuff’ is called Dependency inversion principle (D in SOLID)? Anyway, would it be a time-saver if we did all this stuff for just one report in an effort to allow expansion in the future (future-proofing)?

    2. Is there a mailing list? Really like your writings.

  2. Hi Carlos! Thanks for writing!

    I wouldn’t say that “all this stuff” is specifically talking about Dependency Inversion as it is really discussing the complete concept of SOLID.

    It is my opinion that at any point one should do the minimum to solve the problem at hand – the concept of “future proofing” is therefore quite problematic to me, and needs to be balanced with knowledge of what the future holds and the probability of each of those things in relation to its cost. Therefore, I believe that if there is only one report, then is is better to wait until building the second report to actually extrapolate everything into interfaces, traits, and abstract classes. It’s easy to do this when going from one to two reports, but gets harder if you are going from many to even more reports! Personally, I tend toward doing things like this sooner than later, but wouldn’t do it unless I already have plans to build more reports.

    I’m glad you like my writing, but I don’t operate a mailing list. I do post all new articles to my Twitter feed (@johncurt82) and in the my Facebook Group (@engagedblog). Feel free to subscribe to either of those for instant updates when there are new articles!

Leave a Reply

Your email address will not be published. Required fields are marked *