ebollens [GitHub] / ebollens [LinkedIn] / @ericbollens [Twitter]
Lead Software Architect
UCLA Office of Information Technology's Education and Collaboration Technology Group
Independent Consultant
Bollens Information Technology Services
Senior Partner and Chief Technologist
AnodyneSoft, LLC
network and routing protocols
infrastructure-as-code
frameworks and APIs
web applications
front-end toolkits
I enjoy building internal tools that work
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...
Black box their internals with complexity
Define a "right" way of doing things
Company provides tools and business processes
Consultants operate as independent contractors
Core framework that establishes a baseline
Support for a slew of plug-and-play modules
\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...
There's nothing that does everything we want
We can't pick and choose, so we must write our own
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?
Documentation was over a hundred pages long
Had to know the system inside-and-out to use
Not conducive to our business model!
PHP is a widely-used general-purpose scripting language...
We're basically writing Cocoa, .NET, or the Java library
Give developer what they want, when they want it
and stay out of their way otherwise!
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
Autoloading for seamless extensibility
Plug-and-play execution stack
MVC routing strategies
Output buffers
Persistence layer abstractions
http://ebollens.github.io/php-frameworks
Laravel
http://laravel.com
Evander
https://github.com/ebollens/evander
Evander 2 (dev)
https://github.com/ebollens/evander-dev
and associated vendor/evander
submodules
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';
PHP 5 added a number of magic methods
One of these was __autoload
function __autoload($className) {
include 'lib/'.$className.'.php';
}
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';
});
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 namenew \SomeVendor\Space_Sub\Model\User
require 'SomeVendor/Space/Sub/Model/User.php'
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 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
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();
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';
Define a stack of potential namespaces
class Autoload {
private static $_namespaces = array();
public static function addNamespace($namespace) {
array_unshift(self::$_namespaces, $namespace);
}
// ..
}
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;
}
Class aliasing method
public static function alias($aliasName, $className) {
$aliasNamespace = explode('\\', $aliasName);
$aliasName = array_pop($aliasNamespace);
eval('namespace '.implode('\\', $aliasNamespace).' { class '.$aliasName.' extends '.$className.' { } }');
}
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);
}
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';
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!
github.com/ebollens/evander-autoload/blob/master/
autoloader.php
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
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!
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)
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);
}
$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 */
}
}
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.
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...
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
Let the developer attach
where they want to attach
without modifying the core
or relying on global knowledge
Many frameworks use a scheme like:
index.php?controller/method/arg1/arg2/arg3
For example...
index.php?news/category/tech/newest
$controller = new Controller\News();
$controller->category('tech', 'newest');
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);
}
// ..
}
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());
}
}
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
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!
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 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
Depends completely on the use case!
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
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;
}
}
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
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();
}
}
// ..
}
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();
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;
}
namespace Framework;
class Output {
public static function init() {
ob_start();
}
public static function shutdown() {
ob_end_flush();
}
}
Output::init();
register_shutdown_function('Output::shutdown');
Extensibility!
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
The Evander Output
, View
and Template
Evander 2 (dev) uses event stack for output and template
State that lives beyond the process that created it
Logical representation as objects
Create
Read
Update
Delete
Object represents record in a database table
Bound to existing record or representing new record
Implements CRUD interface plus setters and getters
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
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';
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);
}
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;
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);
}
}
Right now, every interaction is a database query!
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?
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];
}
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];
}
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;
}
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
$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..
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;
}
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...
Or for those less adventurous
https://github.com/ebollens/evander/blob/
master/lib/model/cookie_model.php
Reusable and extensible
Conducive to our mental model
Use a framework to provide baseline functionality
Don't force the user into one way of working
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
ebollens [GitHub] / ebollens [LinkedIn] / @ericbollens [Twitter]