Skip to content

Instantly share code, notes, and snippets.

@tdutrion
Last active February 17, 2017 14:16
Show Gist options
  • Save tdutrion/773bf1af1fffd11fb54f2212049a7e2f to your computer and use it in GitHub Desktop.
Save tdutrion/773bf1af1fffd11fb54f2212049a7e2f to your computer and use it in GitHub Desktop.
Introduction to Oriented Object Programming in PHP - Nicolas Eeckeloo

Visibility reminders

Class attributes should be private or protected, as we do not want to expose the object logic but rather behaviours through public method. How the class works inside is not the business of the developer that uses the library, details of the implementation do not matter for the him/her.

class Book
{
 	public $title;
	public $content;
	
	public function getTitle() : string
	{
		return $this->title;
	}
}

$book = new Book();
$book->title = [];
$book->getTitle();

Fatal error: Uncaught TypeError: Return value of Book::getTitle() must be of the type string, array returned in /in/JN4uJ:10 Stack trace: #0 /in/JN4uJ(16): Book->getTitle() #1 {main} thrown in /in/JN4uJ on line 10

Process exited with code 255.

Encapsulation reminders

Encapsulation refers to the idea that the details of the implementation only matters from inside the class and should never be exposed. As exposed above, the visibility matters to allow encapsulation.

public function getDetails() : string
{
    return '<h1>' . $this->title . '</h1>' . $this->content;
}
public function getDetails() : string
{
    return "<h1>{$this->title}</h1>{$this->content}";
}

Both implementations are slightly different, but that does not matter as they respect the same contract. More complex examples make more sense, this one is just to grasp a very simple idea of the concept.

Single responsibility

Given the following class, criticise why and how the single responsibility is not respected:

class Book
{
    private $title;
    
    private $content

    public function getTitle() : string
    {
        return $this->title;
    }
    
    public function getContent() : string
	{
		return $this->content
	}
	
	public function display()
	{
	    echo $this->content;
	}
}

The Book object is both a data container (contains title and content) – such as an entity/model – and also handle display. These are two different responsibilities.

By removing the display method, we can leave only a single responsibility to the object. This can be done by creating a class that handles anything related to outputting the book's details.

As a slightly more complex, hence more real scenario, we can create an interface for rendering the book, and then two different implementations.

<?php

class Book
{
    private $title;

    public function __construct(string $title)
    {
        $this->title = $title;
    }

    public function getTitle() : string
    {
        return $this->title;
    }
}

interface BookRenderer
{
 	public function display();
}

class BookEchoRenderer implements BookRenderer
{
	private $book;

	public function __construct(Book $book)
	{
		$this->book = $book;
	}

	public function display()
	{
		echo $this->book->getTitle();
	}
}

class BookDumpRenderer implements BookRenderer
{
	private $book;

	public function __construct(Book $book)
	{
		$this->book = $book;
	}

	public function display()
	{
		var_dump($this->book->getTitle());
	}
}

$book = new Book('OOP in PHP');

$echoRenderer = new BookEchoRenderer($book);
$echoRenderer->display();

echo "\n";

$dumpRenderer = new BookDumpRenderer($book);
$dumpRenderer->display();

OOP in practice

Scenario: create a service (a Plain Old PHP Object) with cache and logs capabilities.

A Plain Old PHP Object, sometimes know as POPO, is an instance of a specific class that does not inherit from any other class. It is the most simple type of object you can get in the language.

This example will lead us through different OOP concepts. While we will focus on advantages in a first time, all these concepts will be named properly in a timely manner.

Create a directory to contain the example:

mkdir oop-training-scenario && cd oop-training-scenario

Depend on existing interfaces

The class should have cache and logs capabilities. As previously explained during the PHP FIG presentation, the PHP FIG issued recommendations for both abilities already: PSR-3 for logs and PSR-6 for cache.

Both of these recommandations come with packages that contains the proper interfaces to be interoperable with libraries that follow them.

Use composer to fetch the dependencies:

composer require psr/log
composer require psr/cache

As explained during the Composer training, these commands will generate and update the composer.json and composer.lock files, along with downloading all the dependencies in the vendor folder.

It also generates the vendor/autoload.php file that should be used later on to autoload all of the dependencies classes and optionally classes added to the autoloader configuration in the composer.json.

Service

<?php

include 'vendor/autoload.php';

use Psr\Log\LoggerInterface;
use Psr\Cache\CacheInterface;

class ExampleService
{
	public $repository;
	
	public $logger;
	
	public $cache;
	
	public function setRepository(Repository $repository)
	{
	    $this->repository = $repository;
	}
	
	public function setLogger(LoggerInterface $logger)
	{
	    $this->logger = $logger;
	}
	
	public function setCache(CacheInterface $cache)
	{
	    $this->cache = $cache;
	}
	
	public function getData($id)
	{
		$item = $this->cache->get('foo');
		if (!$item) {
			$item = $this->repository->find($id);
			$this->cache->set('foo', $item);
		}
		$this->logger->info('foo');
		
		return $item;
	}
}

Problems:

  • public properties
  • too many responsibilities
  • temporal coupling: if setters are not called before method, danger => move dependencies to constructor

Updating visibility

In this scenario, it is really simple to update visibilities. Be aware that in production code, changing visibility from permissive to restrictive may cause errors. Therefore, when coding, try to use mostly private attributes (always unless you have a good reason such as data transfer objects for SOAP).

Because these object properties are never called directly, we can refactor and set them private.

private $repository;
	
private $logger;
	
private $cache;

Inject dependencies

The dependency injection principle consist in setting created dependencies in the class (ie. never use the new keyword in a class that is not a factory).

Two types of dependency injection exist:

  • Soft dependencies, using setters, as used above (spoiler: never use it)
  • Hard dependencies, using the __construct method

Soft dependencies expose the code to temporal coupling as previously stated, meaning the execution of a method depends on a call of another method first.

Hard dependencies on the other end provide a way to inject all of the class dependencies in a single location and guarantee the state of the instanciated object (any dependency will be there from the instanciation of the object to its end of life).

<?php

include 'vendor/autoload.php';

use Psr\Log\LoggerInterface;
use Psr\Cache\CacheInterface;

class ExampleService
{
	private $repository;
	
	private $logger;
	
	private $cache;
	
	public function __constructor(Repository $repository, LoggerInterface $logger, CacheInterface $cache)
	{
	    $this->repository = $repository;
	    $this->logger = $logger;
	    $this->cache = $cache;
	}
	
	public function getData($id)
	{
		$item = $this->cache->get('foo');
		if (!$item) {
			$item = $this->repository->find($id);
			$this->cache->set('foo', $item);
		}
		$this->logger->info('foo');
		
		return $item;
	}
}

Split responsibilities

The service currently handles different types of concerns: the business logic (retrieve an element), caching and log. As three responsibilities were found, at least three classes are needed in order to do a proper separation of concerns.

Both inheritance and composition can help with solving these problems. These strategies are discribed thereafter.

Inheritance

The logger and the cache should be moved out of the class. Therefore, we can add the logger to a subclass, and eventually the cache to a subclass of the latter.

[![Hierachical class diagram](http://yuml.me/diagram/plain;dir:TB/class/[ExampleService]^-[ExampleWithLogService], [ExampleWithLogService]^-[ExampleWithCacheAndLogService])](http://yuml.me/diagram/plain;dir:TB/class/edit/[ExampleService]^-[ExampleWithLogService], [ExampleWithLogService]^-[ExampleWithCacheAndLogService])

<?php

include 'vendor/autoload.php';

use Psr\Log\LoggerInterface;
use Psr\Cache\CacheInterface;

class ExampleService
{
	private $repository;
	
	public function __constructor(Repository $repository)
	{
	    $this->repository = $repository;
	}
	
	public function getData($id)
	{
        $item = $this->repository->find($id);

		return $item;
	}
}

class ExampleWithCacheService extends ExampleService
{	
	private $cache;
		
	public function __constructor(Repository $repository, CacheInterface $cache)
	{
        parent::__construct($repository) 
        $this->cache = $cache;
	}
	
	public function getData()
	{
        $data = $this->cache->get('foo');
        if ($data) {
            return $data;
        }
		return parent::getData($id);
	}
}

class ExampleWithCacheAndLogService extends ExampleWithLogService
{	
	private $logger;
		
	public function __constructor(Repository $repository, CacheInterface $cache, LoggerInterface $logger)
	{
	   parent::__construct($repository, $cache) 
		$this->logger = $logger;
	}
	
	public function getData()
	{
		$item = parent::getData($id);
		$this->logger->info('foo');
		return $item;
	}
}

PHP does not allow multiple inheritance, therefore we necessarly end up with a stack of features (ie. there is no way in the code provided to only do the business logic and log only, with no cache).

Composition

Composition class diagram

include 'vendor/autoload.php';

use Psr\Log\LoggerInterface;
use Psr\Cache\CacheInterface;

interface ExampleServiceInterface
{
	public function getData($id);
}

class ExampleLoggerService implements ExampleServiceInterface
{
	private $exampleService;
	
	private $logger;	
	
	public function __constructor(ExampleServiceInterface $service, LoggerInterface $logger)
	{
	    $this->exampleService = $service;
	    $this->logger = $logger;
	}
	
	public function getData($id)
	{
		$item = $this->exampleService->getData($id);
		$this->logger->info('foo');		
		return $item;
	}
}

class ExampleCacheService implements ExampleServiceInterface
{
	private $exampleService;
	
	private $cache;	
	
	public function __constructor(ExampleServiceInterface $service, CacheInterface $cache)
	{
	    $this->exampleService = $service;
	    $this->cache = $cache;
	}
	
	public function getData($id)
	{
		$item = $this->cache->get('foo');
		if ($item) {
			return $item;
		}
		$item = $this->exampleService->getData($id);
		$this->cache->set('foo', $item);
		return $item;
	}
}

class ExampleService
{
	private $repository;
	
	public function __constructor(Repository $repository)
	{
	    $this->repository = $repository;
	}
	
	public function getData($id)
	{
		return $this->repository->find($id);
	}
}

$repository = new Repository();
$exampleService = new ExampleService($repository);

$cache = new Cache();
$exampleCacheService = new ExampleCacheService($exampleService, $cache);

$logger = new Logger();
$exampleCacheLoggerService = new ExampleLoggerService($exampleCacheService, $logger);

$exampleCacheLoggerService->getData(1);

Example: initialisers, awareInterface and traits === danger

Law of Demeter

Out of scope: factory 101

class ExampleServiceFactory
{
    public function __invoke()
    {
        $repository = new Repository();
        $exampleService = new ExampleService($repository);
	
			$cache = new Cache();
			$exampleCacheService = new ExampleCacheService($exampleService, $cache);
			
			$logger = new Logger();
			$exampleCacheLoggerService = new ExampleLoggerService($exampleCacheService, $logger);
			
			return $exampleCacheLoggerService;
    }
}

$factory = new ExampleServiceFactory();
$service = $factory();

Back on track: Do not inject an object to use inside of the object

OK:

class ExampleServiceFactory
{
    public function __invoke($container)
    {
    	$entityManager = $container->get('EntityManager');
		$exampleRepository = $entityManager->get(ExampleRepository::class);
		$exampleService = new ExampleService($exampleRepository);
	
		$cache = new Cache();
		$exampleCacheService = new ExampleCacheService($exampleService, $cache);
			
		$logger = new Logger();
		$exampleCacheLoggerService = new ExampleLoggerService($exampleCacheService, $logger);
			
		return $exampleCacheLoggerService;
    }
}

$factory = new ExampleServiceFactory();
$service = $factory();

KO:

class ExampleServiceFactory
{
    public function __invoke($container)
    {
    	$entityManager = $container->get('EntityManager');
		$exampleService = new ExampleService($entityManager);
	
		$cache = new Cache();
		$exampleCacheService = new ExampleCacheService($exampleService, $cache);
			
		$logger = new Logger();
		$exampleCacheLoggerService = new ExampleLoggerService($exampleCacheService, $logger);
			
		return $exampleCacheLoggerService;
    }
}

class ExampleService
{
	private $repository;
	
	public function __constructor(EntityManager $entityManager)
	{
	    $this->repository = $entityManager->get(ExampleRepository::class);
	}
	
	public function getData($id)
	{
		return $this->repository->find($id);
	}
}

$factory = new ExampleServiceFactory();
$service = $factory();

Immutability

class Book
{
	private $title;
	private $createdAt;
	
	public function __construct(string $title)
	{
		$this->title = $title;
		$this->createdAt = new \DateTime();
	}
	
	public function title() : string
	{
		return $this->title;
	}
	
	public function createdAt() : \DateTime()
	{
		return $this->createdAt;
	}
}

$book = new Book('title');
$newDate = $book->createdAt()->sub('P1D'); //muted
var_dump($book->createdAt()); //initial createdAt + 1 day
class Book
{
	private $title;
	private $createdAt;
	
	public function __construct(string $title)
	{
		$this->title = $title;
		$this->createdAt = new \DateTimeImmutable();
	}
	
	public function title() : string
	{
		return $this->title;
	}
	
	public function createdAt() : \DateTimeImmutable()
	{
		return $this->createdAt;
	}
}

$book = new Book('title');
$newDate = $book->createdAt()->sub('P1D'); //muted
var_dump($book->createdAt()); // initial createdAt
var_dump($newDate); // initial createdAt + 1 day

Example with money (currency, comission)

KO:

class UserAmount
{
	private $amount;
	private $currency;
	
	public function __construct($amount, $currency)
	{
		$this->amount = $amount;
		$this->currency = $currency;
	}
	
	public function add($amount)
	{
		$this->amount += $amount;
		return $this;
	}
}

OK:

class UserAmount
{
	private $amount;
    
	private $currency;
	
	public function __construct($amount, $currency)
	{
		$this->amount = $amount;
		$this->currency = $currency;
	}
	
	public function add($amount)
	{
		return new UserAmount(
			$this->amount + $amount,
			$this->currency
		);
	}
}

Read ocramius (defensive programming especially for this concept) + Michael Gallego

Exceptions

Error management slightly weird in current projects, involving return false or array and so on.

Exceptions vs Errors in PHP7 + throwable

Example of a very simple exception:

try {
    // problem (pdo)
} catch (Exception $exception) {
    // do whatever with the exception
}

Third parameter of Exception construct for stack, rethrow error.

Dump exception and look at what is in it.

Methods (getMessage and so on), useful for logging for instance.

How to raise an exception using throw. Second parameter in constructor is error code.

Describe Exception stack for more specific exceptions (LogicException, InvalidArgumentException, UnexpectedValue, ...).

The stack is a hierachie, so we can catch from specific to generic.

Use custom exception to find quickly where the error comes from (namespace for source package). Custom exceptions help with readability.

Naming conventions, most the custom exceptions should have a semantic naming (ie. AmountThresholdReachedException) that provide details about what problem has been raised.

Thrown exceptions are thrown until caught, so it helps with going back through multiple layers, ie. from db access layer to controller.

When building a library, create an interface that can be caught later on.

Tsi\PaymentClient\Exception\ExceptionInterface;
Tsi\PaymentClient\Exception\InvalidArgumentException;

Using specific exception helps with doing specific actions in catch depending on what happened. Action for db timeout (critical log + notification) might be different from actions to react to a db credential error (connect to another db).

Example with Ogone payment retry, catch only the ogone exception, not the Guzzle Http Client exceptions.

Bad:

class ExampleModel
{
	public $error;
	
	public $data;
	
	public function getData($id)
	{
		// do something
		$data = $repository->find($id);
		if (!$data) {
			$this->error = $error;
			return false;
		}
		$this->data = $data;
		return true;
	}
}

Good:

class ExampleModel
{
	public function getData($id) : data
	{
		// do something
		$data = $repository->find($id);
		if (!$data) {
			throw new SpecificException();
		}
		return $data;
	}
}

Repository Example:

class UserRepository
{
    public function getById($id) : User
    {
        $user = $this->find($id);
        if (!$user) {
            throw new UserDoesNotExistException();
        }
        return $user;
    }
    
    public function exists($id) : bool
    {
        try {
            $this->getById($id);
        } catch (UserDoesNotExistException $exception) {
            return false;
        } catch (Exception $exception) {
            return false;
        }
        return true;
    }
}

Chain catch: top down

class UserRepository
{
    public function getById($id) : User
    {
         $user = $this->find($id);
         if (!$user) {
             throw new UserDoesNotExistException();
         }
         return $user;
    }
    
    public function exists($id) : bool
    {
        try {
            $this->getById($id);
	    } catch (Exception $exception) {
            return false;
        } catch (UserDoesNotExistException $exception) {
            return false; // never reached
        }
        return true;
    }
}

External resources

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment