A Primer for Object-Oriented Programming in Drupal 8

Justin Langley
|
September 1, 2016
Image
Drupal 8 logo in light blue

Drupal 8 fully embraced OOP principles under-the-hood. So now, where to begin?

Object-Oriented Programming in Drupal 8

So if you're like me and have been using Drupal 7 for a couple years now, you've become fairly comfortable with the way Drupal handles itself. The hook system is clever, and gives LOTS of control.

And then you start working on Drupal 8 and suddenly every module has 1,000+ files in it (exaggeration of course, but sometimes it FEELS that way).

Everything is broken down into classes and interfaces and each file itself is normally MUCH smaller than the average .module file in Drupal 7. So, where to begin? Well, if you haven't been exposed to OOP style programming then I suggest reading some of the Basics of OOP in PHP. You should make sure to cover Classes, Interfaces, namespace Statement and Use.

Next we will do a small walkthrough of a code example (be able to create Ball objects with varying properties and print those to HTML) that might help make the OOP principles of PHP seem a little less daunting!

 

Autoloading Classes (Autoloading Classes?)

So, in PHP there is this magic method called __autoload() that would allow you to define how you discover PHP classes within your project structure. This magic method has fallen by the wayside in leu of spl_autoload_register() which allows you to define a collection of callback functions that can find classes.

This whole idea of "autoloading" is just a way to include all the necessary PHP Classes into a single file (in Drupal's case we want to load all Classes into index.php. Which if you open you will see $autoloader = require_once 'autoload.php';. Drupal's own implementation of autoloading can be found if you open autoload.php and follow the rabbit trail).

Moving on!

 

Simple Autoloading

So let's go ahead and set up a simple directory structure so we serve up an index.php file. (this guide does assume you are serving up sites through MAMP (we use docroot as the folder to determine where the Drupal site lives) and I am using a Mac. So some things you made need to interpret.)

cd {directory where my sites are contained} (In my case this is Users/{user}/Sites)
mkdir OOP_PHP
cd OOP_PHP
mkdir docroot
cd docroot
touch index.php

Cool! Now we have a basic site structure and now if I navigate in my browser to docroot.OOP_PHP.dev I see a blank screen. (Your MAMP set up my vary, but once you can navigate to the directory locally and MAMP serves up that blank index.php file you should be set up for the rest).

Open up that index.php and let's add something to it!

<?php

include 'autoload.php';

Awesome. So now we should create that autoload file.

touch autoload.php

Now let's crack it open and put some work in!

<?php
spl_autoload_register('exampleAutoLoader');

function exampleAutoLoader($class) {
  $path = str_replace('\\', '/', $class);
  include $path . '.php';
}

So break let me break this down:

spl_autoload_register('exampleAutoLoader');

Here we are registering the function exampleAutoLoader to be called whenever a PHP file contains the use statement. So if you were to do something like:

<?php

use Example\Title;

The exampleAutoLoader function's $class parameter would be Example\Title.

The function's first line is:

$path = str_replace('\\', '/', $class);

So here we are replacing the \ with /. ( \ needs to first be escaped by a \ that's why there are two in the str_replace function.) That turns Example\Title into Example/Title. The reason we do this is because directory structures use /.

The next line:

include $path . '.php';

Is just going try and add the .php extension to the path we passed. So, ideally, this would mean calling use Example\Title would try and load the file Title.php from index.php/Example/Title.php. Clear as mud yet?

 

Creating some classes

So let's add some classes to try and bring this together.

( from inside the directory where index.php exists )
mkdir Base
cd Base
touch CircleInterface.php
touch Ball.php

Interfaces are an excellent way to have a "definition" for a class. Any methods defined in the interface MUST be implemented by the Class using the interface. Subsequently, all functions in the interface MUST be public. So, for a 2d circle, the only real "construct" we care about is the radius. A circle MUST have a radius. So we should define how to get and set that property right?

<?php
namespace Base;

interface CircleInterface {
/**
 Any object implementing this interface MUST implement these two methods. Notice here there is
 nothing IN the methods, as these are just definitions of WHAT the methods are.
*/
  public function getRadius();

  public function setRadius($radius);
}

So to see this in action go ahead and put this in the Ball.php file:

<?php
namespace Base;

class Ball implements CircleInterface {
  protected $radius;

  function __construct() {
    $this->radius = 5;
  }

  function getRadius() {
    return $this->radius;
  }
}

Notice how we didn't implemenet the setRadius() method? Well now if we go to our index.php file and add the following:

use Base\Ball as Ball;

$ball = new Ball();

... and then we try and go to the site you'll get an error! Grabbing the PHP logs you will see an error like this: PHP Fatal Error: Class Base\Ball contains 1 abstract method and must therefore be declared abstract or implement the remaining methods.

So we need to go back to the Ball.php and implement the setRadius() function!

<?php
namespace Base;

class Ball implements CircleInterface {
  protected $radius;

  function __construct() {
    $this->radius = 5;
  }

  public function getRadius() {
    return $this->radius;
  }

  public function setRadius($radius) {
    $this->radius = $radius;
  }
}

So now let's check our site and it should be much happier now!

 

So with some cleverness we can have the ball generate some basic HTML that will at least show the shape on the page. Here we go!

<?php
namespace Base;

class Ball implements CircleInterface {
  protected $radius;

  public function __construct() {
    $this->radius = 5;
  }

  public function __toString() {
    $diameter = $this->radius * 2;
    $html = "<div style='border: 2px solid #000; border-radius: 50%; width: $diameter; height: $diameter;'></div>";
    $return $html;
  }

  public function getRadius() {
    return $this->radius;
  }

  public function setRadius($radius) {
    $this->radius = $radius;
  }
}

So the __toString() is another magic method that is called on the object when the object is called in any string context (printf, echo, etc etc). So if we call echo $ball on our object, this __toString() function will get called. And here we are just generating some simple HTML with inline styles for presentation purposes.

Cool so now we go back to our index.php file and trying echo'ing out the object!

<?php

include autoload.php

use Base\Ball as Ball;

$ball = new Ball();
echo $ball;

You should a small div in the HTML that looks like a little circle! Awesome sauce. Now we have a visual representation of what is going on with our object.

You can play around here a bit and try calling the setRadius() on the object and adjust it's radius to see the effects.

 

Extending Classes

So one of the absolute best things about OOP is the idea of extending classes. Extending is essentially making a new Class based on an old class (in that it inherits all of it's properties and methods) and allows you to add your own methods or override the previous classes methods! Let's check it out.

Go ahead and create a new directory at the same level as the Base directory.

mkdir Extended
cd Extended
touch BeachBall.php

Now let's extend the Ball class into a new class called BeachBall.

// BeachBall.php file
<?php
namespace Extended;

use Base\Ball;

class BeachBall extends Ball {
  protected $color;

  function __construct() {
    parent::__construct();
    $this->color = 'blue';
  }

  function getColor() {
    return $this->color;
  }

  function setColor($color) {
    $this->color = $color;
  }
}

So now we have a new Class called BeachBall that extends the Ball class and implements some new methods and adds another protected property for setting a color for the object.

So now update your index.php to look like this:

<?php

include 'autoload.php';

use Base\Ball as Ball;
use Extended\BeachBall as BeachBall;

$ball = new Ball();
$beachBall = new BeachBall();

echo $ball;
echo $beachBall;

You should now see TWO circles looking identical! That's because our extended class BeachBall extends the Ball class which contains the __toString() method, so our BeachBall class has that method as well!

(Also you will note how the BeachBall's constructor calls parent::__construct() this simply calls the parent Class's __construct() method if available. So we are essentially saying when creating a new BeachBall we should also call the parent classes constructor incase it initializes anything we need which in our case is setting a default radius.)

So since our BeachBall does allow for a color (and upon creating a new object it sets it to blue) we should override the __toString() method in BeachBall to have the HTML include that color.

<?php
namespace Extended;

use Base\Ball;

class BeachBall extends Ball {
  protected $color;

  function __construct() {
    parent::__construct();
    $this->color = 'blue';
  }

  function __toString() {
    $diameter = $this->radius * 2;
    $html = "<div style='background-color: $this->color; border: 2px solid #000; border-radius: 50%; width: $diameter; height: $diameter;'></div>";
    return $html;
  }

  public function getColor() {
    return $this->color;
  }

  public function setColor($color) {
    $this->color = $color;
  }
}

Now if we navigate back to our page the second circle will be blue! Excellent. So now the last thing I want to cover are the methods on the objects.

 

Working with objects

So now that we have these objects, we can manipulate the instantiated object by calling the methods on the object. Let's make the first ball object have a radius of 100 and see what happens.

<?php

include 'autoload.php';

use Base\Ball as Ball;
use Extended\BeachBall as BeachBall;

$ball = new Ball();
$ball->setRadius(100);

$beachBall = new BeachBall();
echo $ball;
echo $beachBall;

The ball will now be much bigger! The getRadius() function simply set's the radius for $this which is just the current instantiated object. So if we add another Ball and change it's radius it will be different than the first one.

<?php

include 'autoload.php';

use Base\Ball as Ball;
use Extended\BeachBall as BeachBall;

$ball = new Ball();
$ball->setRadius(100);

$secondBall = new Ball();
$secondBall->setRadius(25);

$beachBall = new BeachBall();
echo $ball;
echo $secondBall;
echo $beachBall;

So let's say we wanted the BeachBall to be bigger and change the background color to orange.

<?php

include 'autoload.php';

use Base\Ball as Ball;
use Extended\BeachBall as BeachBall;

$ball = new Ball();
$ball->setRadius(100);

$secondBall = new Ball();
$secondBall->setRadius(25);

$beachBall = new BeachBall();
$beachBall->setRadius(50);
$beachBall->setColor('orange');
echo $ball;
echo $secondBall;
echo $beachBall;

And there we go. Now the last ball will be bigger than before and have a different color!

So there we go! Hopefully this was helpful in getting you on the right track to start digging into Drupal 8 and making awesome things!

Want to talk about how we can work together?

Ryan can help

Ryan Wyse
CEO