Nimble and Flexible

Framework Design

by Eric Bollens

ebollens [GitHub] / ebollens [LinkedIn] / @ericbollens [Twitter]

A bit about me...

Currently...

Lead Software Architect
UCLA Office of Information Technology's Education and Collaboration Technology Group

Independent Consultant
Bollens Information Technology Services

Formerly...

Senior Partner and Chief Technologist
AnodyneSoft, LLC

From layer 3 on up...

network and routing protocols

infrastructure-as-code

frameworks and APIs

web applications

front-end toolkits

I enjoy building internal tools that work

So tonight, let's talk...

Frameworks

What is a framework?

Framework on Dictionary.com:

1. a skeletal structure designed to support or enclose something.
2. a frame or structure composed of parts fitted and joined together.

Software Framework on Wikipedia:

In computer programming, a software framework is an abstraction in which software providing generic functionality can be selectively changed by additional user written code, thus providing application specific software...

But many frameworks also...

Black box their internals with complexity

Define a "right" way of doing things

Is this the right approach?

A Case Study

A "Federated" consulting firm...

Company provides tools and business processes

Consultants operate as independent contractors

Need a stack whereby...

Core framework that establishes a baseline

Support for a slew of plug-and-play modules

So let's build a proprietary edge!

\Core\System
execution manager and object container

\Core\Resource\*
namespace of all base objects

\Core\Asset, \Core\Cache, \Core\DB, \Core\Error, \Core\HTTP\Request, \Core\HTTP\Response, \Core\Input, \Core\Manager, \Core\IO\Router, \Core\Session, \Core\Template, ...
and about 40 more...

\Core\Model\Cookie, \Core\Model\DBO, \Core\Model\Filesystem\File, \Core\Model\Filesystem\Directory, \Core\Model\Filesystem\Symlink, \Core\Filesystem\File, ...
and so many more to model everything...

\Core\Module\Manager, \Core\Module\Mediator, \Core\Module\Metadata, \Core\Module\Controller
to manage modules

\Module\Auth\Identity, \Module\User\Model\User, \Module\User\Controller\Auth, ...
and a slew of others for each module...

And the ability to override anything at the application level...

Problem 1: Reinventing the Wheel

There's nothing that does everything we want

We can't pick and choose, so we must write our own

Problem 2: Complexity

The structure of core is complex

With modules, it gets exponentially worse

And application-level overriding makes it a nightmare

How does one keep track of it all?

Problem 3: Learning Curve

Documentation was over a hundred pages long

Had to know the system inside-and-out to use

Not conducive to our business model!

Problem 4: This isn't PHP!

PHP is a widely-used general-purpose scripting language...

We're basically writing Cocoa, .NET, or the Java library

While an extreme case...

This is a systemic problem!

How can we do things differently?

Give developer what they want, when they want it

and stay out of their way otherwise!

At our firm...

Developers loved the new approach

They could use what they knew

and learn more when they needed it

and use their own ways when they wanted

Giving freedom back

with a look at some internals

Overview

Autoloading for seamless extensibility

Plug-and-play execution stack

MVC routing strategies

Output buffers

Persistence layer abstractions

Slides At

http://ebollens.github.io/php-frameworks


Examples From

Laravel
http://laravel.com

Evander
https://github.com/ebollens/evander

Evander 2 (dev)
https://github.com/ebollens/evander-dev
and associated vendor/evander submodules

Autoloading

In the Early Days

Early OO PHP used include and require

require 'lib/db.php';
require 'lib/template.php';

Bulk includes through a standard library file

require 'lib.php';

__autoload Magic Method

PHP 5 added a number of magic methods

One of these was __autoload

function __autoload($className) {
    include 'lib/'.$className.'.php';
}

Autoload Stack

PHP 5.1.2+ added an autoload stack

function autoload($className) {
    include 'lib/'.$className.'.php';
}
spl_autoload_register('autoload');

Or an anonymous function in PHP 5.3+

spl_autoload_register(function ($className) {
    include 'lib/'.$className.'.php';
});

PSR-0

Fully-qualified namespace and class

\VENDOR_NAME\(NAMESPACE\)CLASS_NAME

In order to autoload a class, filename ascertained as...

  • / converted to DIRECTORY_SEPARATOR
  • _ converted to DIRECTORY_SEPARATOR
  • .php appended after class name
new \SomeVendor\Space_Sub\Model\User
require 'SomeVendor/Space/Sub/Model/User.php'

Implementing PSR-0

function autoload($className) {
    $className = ltrim($className, '\\');
    $fileName  = '';
    $namespace = '';
    if ($lastNsPos = strripos($className, '\\')) {
        $namespace = substr($className, 0, $lastNsPos);
        $className = substr($className, $lastNsPos + 1);
        $fileName  = str_replace('\\', DIRECTORY_SEPARATOR, $namespace) 
                        . DIRECTORY_SEPARATOR;
    }
    $fileName .= str_replace('_', DIRECTORY_SEPARATOR, $className) . '.php';

    require $fileName;
}

Using a PSR-0 Class

Using a session provided by a vendor

$session = new \SomeVendor\Session\Model\Session();

The use keyword simplifies references

use \SomeVendor\Session\Model as Model;
// ..
$session = new Model\Session();

With multiple vendors, use must be more specific

use \SomeVendor\Session\Model\Session as Session;
// ..
$session = new Session();

This looses Model subspace and is all very rigid

Class Aliasing

Instead of use, let's build a runtime equivalent...

function generate_class_alias($aliasName, $className) {
    $aliasNamespace = explode('\\', $aliasName);
    $aliasName = array_pop($aliasNamespace);
    eval('namespace '.implode('\\', $aliasNamespace)
            .' { class '.$aliasName.' extends '.$className.' { } }');
}
namespace Framework\Model;
class Session {
    // ..
}
generate_class_alias('Model\Session', '\SomeVendor\Session\Model\Session');
// ..
$user = new Model\Session();

Autoload Aliasing

Suppose we have a vendor class that the framework extends

namespace SomeVendor\Session\Model;
class Session {
    // ..
}
namespace Framework\Model;
class Session extends SomeVendor\Session\Model\Session {
    // ..
}

Resolution at runtime with autoload and class aliasing

spl_autoload_register('Autoload::load');
Autoload::addNamespace('SomeVendor\\Session');
Autoload::addNamespace('Framework');
$session = new Model\Session();
get_parent_class($session) == '\Framework\Model\Session';

Autoload Aliasing

Define a stack of potential namespaces

class Autoload {

    private static $_namespaces = array();

    public static function addNamespace($namespace) {
        array_unshift(self::$_namespaces, $namespace);
    }

    // ..
}

Autoload Aliasing

Tweak PSR-0 to return false on missing class file

public static function psr0($className) {
    $className = ltrim($className, '\\');
    $fileName  = ''; $namespace = '';
    if ($lastNsPos = strripos($className, '\\')) {
        $namespace = substr($className, 0, $lastNsPos);
        $className = substr($className, $lastNsPos + 1);
        $fileName  = str_replace('\\', DIRECTORY_SEPARATOR, $namespace)
                                                       .DIRECTORY_SEPARATOR;
    }
    $fileName .= str_replace('_', DIRECTORY_SEPARATOR, $className) . '.php';
    if(!file_exists($fileName)) return false;
    require_once $fileName;
    return true;
}

Autoload Aliasing

Class aliasing method

public static function alias($aliasName, $className) {
    $aliasNamespace = explode('\\', $aliasName);
    $aliasName = array_pop($aliasNamespace);
    eval('namespace '.implode('\\', $aliasNamespace).' { class '.$aliasName.' extends '.$className.' { } }');
}

Autoload Aliasing

Add aliasing to autoload when PSR-0 fails

public static function load($className) {
    if(!self::psr0($className))
        foreach(self::$_namespaces as $namespace)
            if(self::psr0('\\'.$namespace.'\\'.$className))
                return self::alias($className, 
                                   '\\'.$namespace.'\\'.$className);
}

Seamless Substitution

Suppose we override Framework\Model\Session

namespace App\Model;
class Session extends Framework\Model\Session {
    // ..
}

Simply add App to the autoload stack

spl_autoload_register('Autoload::load');
Autoload::addNamespace('SomeVendor\\Session');
Autoload::addNamespace('Framework');
Autoload::addNamespace('App');

Seamlessly substituted throughout the application

$session = new Model\Session();
get_parent_class($session) == '\App\Model\Session';

Why?

Framework can extend on vendor classes

Application can extend vendor classes or rewrite them

No code changes necessary for substitution

Give the developer what they want, when they want it...

And stay out of their way otherwise!

Example

github.com/ebollens/evander-autoload/blob/master/
autoloader.php

github.com/ebollens/evander-dev/blob/master/
index.php

Execution Stack

Application Organization

Individual scripts

Operations are encapsulated within multiple PHP files

All PHP files load libraries of common functionality

Model-view-controller or similar

All requests routed through a single script

Router delegates to controller objects and methods

How do you add to execution?

Buffers, caches, logs, hooks, ...

Individual scripts

Modify the common library with new functionality

Model-view-controller or similar

Modify the single script all requests route through

But this means changing a core framework file!

Observer Pattern in SPL

SplObserver {
    abstract public void update ( SplSubject $subject )
}
SplSubject {
    abstract public void attach ( SplObserver $observer )
    abstract public void detach ( SplObserver $observer )
    abstract public void notify ( void )
}
$subject = new Subject();
$observer = new Observer();
$subject->attach($observer);
$subject->notify(); 
// -> calls update($subject)

Observable Execution

class System {
    private $_observers = array();
    public function attach(SystemObserver $observer) { 
        $this->_observers[] = $observer;
    }
    public function detach(SystemObserver $observer) { 
        /* .. */ 
    }
    public function execute($event) { 
        foreach($this->_observers as $observer)
            $observer->execute($event);
    }
}
interface SystemObserver {
    public abstract function execute($event);
}

Observable Execution

$SYSTEM = new System();
/* .. */
$SYSTEM->attach(new Template());
/* .. */
$SYSTEM->execute('preprocess');
$SYSTEM->execute('execute');
$SYSTEM->execute('prerender');
$SYSTEM->execute('render');
class Template implements SystemObserver {
    public function execute($event) {
        switch($event) {
            case 'preprocess': ob_start(); break;
            case 'render':
                $content = ob_get_contents();
                ob_end_clean();
                echo $this->render($contents);
        }
    }
    public function render($contents) { 
        /* return contents in template */ 
    }
}

What do we do with this?

Configuration file sets System observers

Observers are attached before System events execute

Events are passed to Observers, which act accordingly

Good for buffers, caches, logs, hooks, etc.

Events In the Wild

Laravel provides a robust events infrastructure

Event::listen('laravel.started: application', function(){});
Event::listen('laravel.resolving', function($type, $object){});
Event::listen('laravel.controller.factory', function($controller){});
Event::listen('laravel.view.loader', function($bundle, $view){});
Event::listen('laravel.view.loader', function($view){});
Event::listen('laravel.view.filter', function($content, $path){});
Event::listen('laravel.done', function($response){});

And many more...

Event-based Execution in the Wild

Evander 2 (dev) provides completely event-based execution

$application = new Application();
$application->attach('response', HTTP::response());
$application->attach('router', new Router());
// ..
$application->execute('initialize');
$application->execute('execute');
$application->execute('render');
$application->execute('shutdown');
class HTTP_Response {
    public function __application_initialize() {
        // start buffering HTTP body
    }
    public function __application_render() {
        // send HTTP body
    }
    // ..
}

github.com/ebollens/evander-core/blob/master/
application.php

Why?

Let the developer attach

where they want to attach

without modifying the core

or relying on global knowledge

MVC Routing

Implicit Routing

Many frameworks use a scheme like:

index.php?controller/method/arg1/arg2/arg3
  • First parameter defines the controller
  • Second parameter defines the method
  • Remaining parameters define the arguments

For example...

index.php?news/category/tech/newest
$controller = new Controller\News();
$controller->category('tech', 'newest');

Implementing Implicit Routing

Segment processing

class Router {
    private $_segs;
    const D_CONT = 'home'; const D_METH = 'index';
    public function __construct(array $segments) {
        $this->_segs = $segments;
    }
    public function getController() { 
        return isset($this->_segs[0]) ? $this->_segs[0] : self::D_CONT;
    }
    public function getMethod() { 
        return isset($this->_segs[1]) ? $this->_segs[1] : self::D_METH;
    }
    public function getArguments() { 
        return array_slice($this->_segs, 2);
    }
    // ..
}

Implementing Implicit Routing

Controller method delegation

class Router {
    // ..
    public function execute() {
        $class = '\\Controller\\'.$this->getController();
        $object = new $class();
        $callback = array($object, $this->getMethod());
        call_user_func_array($callback, $this->getArguments());
    }
}

Hierarchical MVC

In large systems, controllers may be divided into modules

This makes implicit routing rules more complex

Simplest differentiator is a different separator

index.php?m:controller/method/arg1/arg2/arg3

Still, there's rigidity in this approach

HTTP Methods

RESTful applications use multiple HTTP methods

Convention required in controller method names

class News {
    public function get_category(){}
    public function post_category(){}
    public function put_category(){}
    public function delete_category(){}
}

Again, rigidity!

Explicit Routing

Greater flexibility as no forced conventions - not even MVC

Route::any('/news/category/(:string)/(:string?)', 
           function($name, $filter = false){ /* .. */ });

Clear support for different HTTP methods

Route::get('/news/category/(:string)/(:string?)', 
           function($name, $filter = false){ /* .. */ });
Route::post('/news/category', function(){ /* .. */ });
Route::put('/news/category/(:string)', function($name){ /* .. */ });
Route::delete('/news/category/(:string)', function($name){ /* .. */ });

Explicitly route to controllers

Route::get('/news/category', 'news@category');

This flexibility comes at the cost of verbosity

Explicit Routing with Implicit Routes

Explicit routing with patterns is a superset of implicit

Route::any('(.*)', function($route){
    // ..
});

Simplify implementation with controller support

Route::controller('news');

Reduce all routes to a single implicit-like convention

Route::controller(Controller::detect());

Simplicity at the cost of flexibility

Which is best?

Depends completely on the use case!

Buffer Output

PHP Output Buffer

Functionality provided by ob_* functions

ob_start();
echo 'z';
ob_clean(); // 'z' will never print
echo 'a';
ob_flush(); // 'a' prints
echo 'b';
echo 'c';
ob_end_flush(); // 'bc' prints

Capturing non-return-value function output

ob_start();
var_dump();
$var_dump = ob_get_contents();
ob_end_clean();

Even the include and require functions

Views via Output Buffer

Object representation of a file with variables support

$view = new View($name);
$view->foo = 'bar';
$output = $view->render();
class View {
    private $_filename; private $_data = array();
    public function __construct($filename){
        $this->_filename = $filename; 
    }
    public function __set($name, $val){ 
        $this->_data[$name] = $val; 
    }
    public function render(){
        ob_start();
        extract($this->_data);
        include($this->_filename);
        $contents = ob_get_contents();
        ob_end_clean();
        return $contents;
    }
}

Buffer HTTP Response Body

Why?

Write a header at any time

Clear buffer and reroute internally

Full page response caching

Why not?

Client must wait until server is done

Confusing if not transparent

In the Wild

Laravel's Application::Run and Response::Send

public function run(){
    $response = $this->dispatch($this['request']);
    // ..
    $response->send();
    // ..
}
public function send(){
    // ..
    $previous = null;
    $obStatus = ob_get_status(1);
    while (($level = ob_get_level()) > 0 && $level !== $previous) {
        $previous = $level;
        if ($obStatus[$level - 1] && isset($obStatus[$level - 1]['del']) && $obStatus[$level - 1]['del']) {
            ob_end_flush();
        }
    }
    // ..
}

Don't fight the Language System!

Hook into the shutdown routine

ob_start();
register_shutdown_function(function(){
    ob_end_clean();
});

Still behaves like any other output buffer

ob_flush();
ob_clean();

Easy Rerouting

In MVC, sometimes want to reroute after action

namespace Controller;
class News {
    public function postArticle(){
        // ..
        if($this->_validateArticle($_POST)){
            $newsId = $this->_createArticle($_POST);
            Router::reroute('news/article/'.$newsId);
        }
        // ..
    }
    // ..
}

This can be achieved without header

public function reroute($route){
    ob_clean();
    Router::execute($route);
    exit;
}

Objectify the Output Buffer

namespace Framework; 
class Output {
    public static function init() {
        ob_start();
    }
    public static function shutdown() {
        ob_end_flush();
    }
}
Output::init();
register_shutdown_function('Output::shutdown');

Why Objectify the Object Buffer?

Extensibility!

Adding Templating

Assuming autoloading with namespace-based class aliases...

Autoload::addNamespace('Framework');
Autoload::addNamespace('Application');
namespace Application;
class Output extends Framework\Output {
    public static function shutdown() {
        $content = ob_get_contents();
        ob_end_clean();
        Template::set_var('CONTENT', $content);
        $template = Template::render();
    }
}

Added behavior without modifying framework code

In the Wild

The Evander Output, View and Template

Evander 2 (dev) uses event stack for output and template

Persistence Layers

Persistence

State that lives beyond the process that created it

  • Database records
  • Filesystem entities
  • Cookies (and sessions)

Logical representation as objects

CRUD Lifecycle

Create

Read

Update

Delete

Objectifying CRUD

in databases

Active Record

Object represents record in a database table

Bound to existing record or representing new record

Implements CRUD interface plus setters and getters

Implementing Active Record (badly)

namespace Model;
class Active_Record {
    private $_table; private $_key; private $_col;
    public function __construct($table, $col, $key = false){
        $this->_table = $table; 
        $this->_col = $col; 
        $this->_key = $key;
    }
    // ..
}
public function read($col){
    return DB::value('SELECT %s FROM %s WHERE %s = %s',
        $col, $this->_table, $this->_col, $this->_key);
}
public function update($col, $val){
    return DB::query('UPDATE %s SET %s = %s WHERE %s = %s',
        $this->_table, $col, $val, $this->_col, $this->_key);
}

create() and delete() implemented similarly

Using the language system

This isn't very elegant

$rec = new Model\Active_Record('user', 'id', 1);
$rec->firstname = 'John';
$rec->lastname = 'Doe';

Aren't columns like object properties?

$rec = new Model\Active_Record('user', 'id', 1);
$rec->firstname = 'John';
$rec->lastname = 'Doe';

Magic Methods

public function __get($col) { 
    return DB::value('SELECT %s FROM %s WHERE %s = %s',
            $col, $this->_table, $this->_col, $this->_key);
}
public function __set($col, $val) { 
    return DB::query('UPDATE %s SET %s = %s WHERE %s = %s',
            $this->_table, $col, $val, $this->_col, $this->_key);
}

Basic Extensibility

namespace Model;
class Active_Record {
    public function __construct($table, $col, $key = false){
        // ..
    }
    // ..
}
namespace Model;
class User extends Model\Active_Record {
    public function __construct($key = false, $col = 'id'){
        parent::__construct('user', $col, $key);
    }
}
$generic = new Model\Active_Record('user', 'id', 1);
$specific = new Model\User(1);
$generic->id == $specific->id;

Late Static Binding Extensibility

namespace Model;
class User extends Model\Extensible_Active_Record {
    protected static $_table = 'user';
    protected static $_col = 'id';
}
namespace Model;
class Extensible_Active_Record extends Model\Active_Record {
    protected static $_table;
    protected static $_col;
    public function __construct($key = false, $col = false){
        parent::__construct(static::$_table, 
                            $col ? $col : static::$_col, 
                            $key);
    }
}

Fixing Wretched Performance

Right now, every interaction is a database query!

Buffered Reads

private $_rBuffer = false;
public function __construct($table, $col, $key = false){
    $this->_table = $table; 
    $this->_col = $col;
    $this->_key = $key; 
    if($key)
        $this->_rBuffer = DB::row('SELECT %s FROM %s WHERE %s = %s',
                            $col, $this->_table, $this->_col, $this->_key);
}
public function __get($col){
    $this->_fetch();
    return $this->_rBuffer[$col];
}

But what if we never read the record?

Lazy Initialization

private $_rBuffer = false;
public function __construct($table, $key, $col){
        $this->_table = $table; 
        $this->_key = $key; 
        $this->_col = $col;
}
private function _fetch(){
    if(!$this->_rBuffer)
        $this->_rBuffer = DB::row('SELECT %s FROM %s WHERE %s = %s',
                            $col, $this->_table, $this->_col, $this->_key);
}
public function __get($col){
    $this->_fetch();
    return $this->_rBuffer[$col];
}

Buffered Writes

private $_wBuffer = array();
public function __set($col, $val){
    $this->_wBuffer[$col] = $val;
}
public function update(){
    DB::update($this->_wBuffer, $this->_col, $this->_key);
    if($this->_rBuffer)
        $this->_rBuffer = array_merge($this->_rBuffer, $this->_wBuffer);
    $this->_wBuffer = array();
}
public function read($col){
    $this->_fetch();
    return isset($this->_wBuffer[$col]) 
                ? $this->_wBuffer[$col] 
                : $this->_rBuffer[$col];
}

Demand Load

Add a function to load known data

public function load(array $data){
    $this->_rBuffer = $data;
}

Now we can build multiple Active Records with one DBq

$rows = DB::rows('SELECT * FROM users');
$records = array();
foreach($rows as $row){
    $record = new Model\Active_Record('users', 'id', $row['id']);
    $record->load($row);
    $records[] = $record;
}

Putting it all together

CRUD operations

Magic setters and getters

Read and write buffers

Lazy initialization and demand loading

Example here is a toy, but it's a common pattern

A complete example...

https://github.com/ebollens/evander/blob/
master/lib/model/active_record_model.php

Objectifying CRUD

in the file system

Not all that different from databases

$file = new Model\File($path);
$file->create($content);
$content = $file->read();
$file->update($newContent);
$file->delete();
$directory = new Model\Directory($path);
$directory->create();
$files = $directory->read();
$directory->delete();

But there's more to the filesystem than that..

What makes a filesystem node?

abstract class Filesystem_Model {
    private $_path;
    public function __construct($path){ $this->_path = $path; }
    public function get_path(){ return $this->_path; }
    public function get_group() { /* .. */ }
    public function set_group($group, $recursive = false){ /* .. */ }
    public function get_owner(){ /* .. */ }
    public function set_owner($owner, $recursive = false){ /* .. */ }
    public function get_mode(){ /* .. */ }
    public function set_mode($octal, $recursive = false){ /* .. */ }
    public abstract function move($newpath, $allow_overwrite = false);
    public abstract function copy($newpath, $allow_overwrite = false);
    public abstract function delete($must_exist = true);
    public function get_parent_directory_name(){
        return $this->get_path() != '/' 
                    ? dirname($this->get_path()) : false;
    }
    public function get_parent_directory(){
        return ($path = $this->get_parent_directory_name()) !== false
                    ? new Directory_Model($path) : false;
    }

Distinct File Operations

class File_Model extends Filesystem_Model {
    public function move($newpath, $allow_overwrite = false){ /* .. */ }
    public function copy($newpath, $allow_overwrite = false){ /* .. */ }
    public function delete($must_exist = true){ /* .. */ }
    public function read($allow_create = false){ /* .. */ }
    public function touch($time = null, $atime = null){ /* .. */ }
    public function write($data, $allow_create = true){ /* .. */ }
    public function append($data, $allow_create = true){ /* .. */ }
}

And similar for directories and symlinks...

In the Wild

Objectifying CRUD

for cookies

Exercise for the curious...

Or for those less adventurous

https://github.com/ebollens/evander/blob/
master/lib/model/cookie_model.php

Why Persistence Objects?

Reusable and extensible

Conducive to our mental model

Where from here?

Staying out of the way

Use a framework to provide baseline functionality

Don't force the user into one way of working

Recap

  • Autoloaders - seamless substitution
  • Observable execution - plug-and-play extensibility
  • Routing approaches - implicit, explicit or hybrid
  • Output buffering - encapsulating output
  • Persistence layer - modeling with objects

Microframeworks

Focus on doing one thing well

Common in language systems like Ruby and Node.js

Made viable in PHP with Composer

Great because developers can mix-and-match components

Laravel 4 is a prime example

Evander 2 will be built this way

THANK YOU

Any Questions?

ebollens [GitHub] / ebollens [LinkedIn] / @ericbollens [Twitter]



http://ebollens.github.io/php-frameworks