I've used some libraries before and there always seems to be some magic under the hood. My intent of this project was not to build a better framework than others out there (it isn't!), but to understand each line of code (I mostly do!).
I hope I am not asking too much, but would like a high level critique of my attempt to put together various concepts. I am by no means expecting a line by line review, but feedback that I am basically "doing it right". If there is also any constructive criticism you would like to give regarding any of the details, I will surely accept that as well.
A few specific topics I would like to here your opinions on are:
- My use of interfaces.
- Namespace and autoloading.
- Dependency injection and the use of pimple. I may need to get rid of pimple and implement this scope myself to ensure I truly understand dependency injection.
- Whether I am applying the controller concept correctly. I still question whether a controller should be passed to a router or the reverse or neither.
- My use of the request and response classes. I might be confused between a response and a view.
- My approach to routes.
- How I deal with error handling. I think this portion surely could be improved.
- My approach to authentication.
- Whether there are any common programming concepts which should be used in an application such as this one but are not being used.
Thank you in advance for any comments you are willing to give.
<?php
//namespace Michael\SimpleRouter;
use Michael\SimpleRouter;
use Michael\SimpleRouter\Request;
use Michael\SimpleRouter\Response;
use Michael\EmbeddedWebserver\Controllers;
session_start();
error_reporting(E_ALL);
ini_set('display_startup_errors', 1);
ini_set('display_errors', 1);
spl_autoload_register(function ($class) {
$prefix = 'Michael\\';
$base_dir = __DIR__ . '/../src/';
$len = strlen($prefix);
if (strncmp($prefix, $class, $len) !== 0) {
return;
}
$relative_class = substr($class, $len);
$file = $base_dir . str_replace('\\', '/', $relative_class) . '.php';
if (file_exists($file)) {
require $file;
}
});
// TBD whether default routes should be added by extending Pimple or in router (which is defined after user defined routes are set so much check them)
// Should I even bother using pimple?
require_once('../src/SimpleRouter/Pimple.php');
$c = new \Pimple();
//$c = new Container();
//Originally, I was planning on passing the view to pimple, but why bother?
//$c['view'] = function ($c) {return new SimpleRouter\View(__DIR__.'/../src/EmbeddedWebserver/templates');};
$c['template']=__DIR__.'/../src/EmbeddedWebserver/templates';
//Authority not sent to constructor thus use default 0 required authority
$router = new SimpleRouter\Router($c);
$auth=(new Controllers\Logon)->getAuthorityLevel();
$router->setAuthUser($auth);
//Routes are regex. # Deliminator to be applied by router and # are not allowed in regex pattern.
$router->get('^/logon$', function(Request $request, Response $response){
return($response->html('logon.html',[]));
});
$router->post('^/logon$', function(Request $request, Response $response){
$controller=new Controllers\Logon();
if($controller->validCredentials()) {
$controller->setAuthorityLevel(1);
return $response
->html('page.html',(new Controllers\Page)->getData($request))
->redirect('/page1');
}
else {
return($response->html('logon.html',[]));
}
});
$router->setAuthRequired(1,'/logon'); //subqential routes require authority value of 1
$router->delete('^/logon$', function(Request $request, Response $response){
(new Controllers\Logon)->setAuthorityLevel(0);
return($response->json(['success']));
});
$router->get('^/$|^/page1$', function(Request $request, Response $response){ //Both / and /page1
return($response->html('page.html',array_merge((new Controllers\Page)->getData($request),['page'=>'Page 1'])));
});
$router->get('^/page2$', function(Request $request, Response $response){
return($response->html('page.html',array_merge((new Controllers\Page)->getData($request),['page'=>'Page 2'])));
});
$router->get('^/page3$', function(Request $request, Response $response){
return($response->html('page.html',array_merge((new Controllers\Page)->getData($request),['page'=>'Page 3'])));
});
$router->all('^/test$', function(Request $request, Response $response){
return($response->json((new Controllers\Page)->getData($request)));
});
$router->all('^/test/(\w+)$', function(Request $request, Response $response){
return($response->json((new Controllers\Page)->getData($request)));
});
$router->all('^/test/(\w+)/(\d+)$', function(Request $request, Response $response){
return($response->json((new Controllers\Page)->getData($request)));
});
echo $router->run(); //Headers will be set in View if appropriate.
<?php
namespace Michael\SimpleRouter;
interface RouterInterface
{
// Should _constructor be included? public function __construct($c, array $auth=[]);
//Add callback for given HTTP method (all adds for each). $auth overrides the required authority for the given route
public function get($pattern, $callback, array $auth=[]);
public function post($pattern, $callback, array $auth=[]);
public function put($pattern, $callback, array $auth=[]);
public function delete($pattern, $callback, array $auth=[]);
public function all($pattern, $callback, array $auth=[]);
//Updates the default authority
public function setAuthRequired($authRequiredLevel,$unauthUrl,$unauthMethod=NULL);
//Gets authority values
public function getAuthRequired();
//Sets users authority
public function setAuthUser($auth);
//Gets users authority
public function getAuthUser();
//Execute the route, set any headers, and return the results
public function run();
}
<?php
namespace Michael\SimpleRouter;
interface RequestInterface
{
public function getMethod();
public function getUri();
//Returns an unassociated array in the same order as the pattern
public function getParams();
//Return $_GET, $_POST, or PUT/DELETE data
public function getData();
//public function isAjax(); //Currently private
}
<?php
namespace Michael\SimpleRouter;
interface ResponseInterface
{
//Should this class really have view functionallity or should view be injected?
//All methods except for render() and error() return $this to allow chaining
// Redirect browser
public function redirect($location);
// Create HTML or JSON
public function html($template, $arr, $code=200);
public function json($data, $code=200);
// Display error in HTML or JSON as appropriate
public function error($msg,$code);
// Return created HTML or JSON
public function render();
}
<?php
namespace Michael\SimpleRouter;
class Router implements RouterInterface {
private $routes = ['GET'=>[],'POST'=>[],'PUT'=>[],'DELETE'=>[]];
private $uri;
private $method;
private $lastEndpoint=[false,false]; //Error if repeating recursive endpoint
private $authValue=0; //The users authority
private $authRequiredLevel=0; //0 is unrestricted, and incrementing higher values require higher authority
private $unauthUrl='/'; //URL to utilize if unathorized access level
private $unauthMethod='GET';
public $container = [];
public function __construct($c, array $auth=[])
{
$this->container=$c;
//remove query (duplicated in $_GET), index.php (just case), and then trailing backslash
$this->uri = preg_replace('/index.php$/', '', explode('?', strtolower($_SERVER['REQUEST_URI']))[0]);
if(strlen($this->uri)>1) $this->uri=rtrim($this->uri,'/'); //uri will be: /foo/bar
$this->method=$_SERVER['REQUEST_METHOD'];
syslog(LOG_INFO,"Router Constructor: $this->uri $this->method");
//$auth defines the default authority requirements and default fallback page
if(isset($auth['authRequiredLevel'])) $this->authRequiredLevel=$auth['authRequiredLevel'];
if(isset($auth['unauthUrl'])) $this->unauthUrl=$auth['unauthUrl'];
if(isset($auth['unauthMethod'])) $this->unauthMethod=strtoupper($auth['unauthMethod']);
if(isset($auth['authValue'])) $this->authValue=$auth['authValue']; //Users authority level
set_exception_handler(function($exception) use ($c){
//Nothing to return to, so echo results?
exit($c['phpErrorHandler'](new Request(), new Response($this->container['template']), $exception));
});
set_error_handler(function($errno, $errmsg, $filename, $linenum) {
if (!(error_reporting() & $errno)) {
return;
}
throw new \ErrorException($errmsg, 0, $errno, $filename, $linenum);
});
//Add default services. TBD whether it would be more appopriate to extend Pimple to add these. Is this the correct way to do this?
$this->addToContainer('missingErrorHandler',function($c){
return function ($request, $response) {
return $response->error($request->getUri().' with method '.$request->getMethod().' does not exist on this server.',404);
};
});
$this->addToContainer('phpErrorHandler',function($c){
return function ($request, $response, $e) {
return $response->error($e->getFile()." (".$e->getline().'): '.$e->getMessage(),400);
};
});
}
//Add to container with default option not to override (used by route constructor to add default services).
private function addToContainer($name, $callback, $override=false) {
if($override || empty($this->container[$name])) {
$this->container[$name] = $callback;
}
}
/* Add route pattern, callback, and auth for each method.
Auth is an array specifing the required user authority to access the route and the url and http method to respond with if not authorized
*/
public function get($pattern, $callback, array $auth=[]) {
$this->setRoute('GET', $pattern, $callback, $auth);
}
public function post($pattern, $callback, array $auth=[]) {
$this->setRoute('POST', $pattern, $callback, $auth);
}
public function put($pattern, $callback, array $auth=[]) {
$this->setRoute('PUT', $pattern, $callback, $auth);
}
public function delete($pattern, $callback, array $auth=[]) {
$this->setRoute('DELETE', $pattern, $callback, $auth);
}
public function all($pattern, $callback, array $auth=[]) {
$this->setRoute(['GET','POST','PUT','DELETE'], $pattern, $callback, $auth);
}
private function setRoute($methods, $pattern, $callback, $auth) {
if(strpos($pattern, '#')!==FALSE) {
//trigger_error('"#" are not allowed in route pattern regex.');
throw new \ErrorException('"#" are not allowed in route pattern regex.');
}
$authRequiredLevel=isset($auth['authRequiredLevel'])?$auth['authRequiredLevel']:$this->authRequiredLevel;
$unauthUrl=isset($auth['unauthUrl'])?$auth['unauthUrl']:$this->unauthUrl;
$unauthMethod=isset($auth['unauthMethod'])?$auth['unauthMethod']:$this->unauthMethod;
foreach((array)$methods as $method) {
$this->routes[$method][$pattern] = ['callback'=>$callback,'authRequiredLevel'=>$authRequiredLevel,'unauthUrl'=>$unauthUrl,'unauthMethod'=>$unauthMethod];
}
}
/* USER ACCESS
All routes have a given authority integer which is required to access.
setAuthRequired() or in the constructor will set the required authority (default of 0) as well as the url and http method if user isn't authorized.
After being set, it will be applied to all routes created after being setting.
Users also have a given integer authority level which must be equal or greater than the route authority
*/
public function setAuthRequired($authRequiredLevel,$unauthUrl,$unauthMethod=NULL) {
if($authRequiredLevel)$this->authRequiredLevel=$authRequiredLevel;
if($unauthUrl)$this->unauthUrl=$unauthUrl;
if($unauthMethod)$this->unauthMethod=strtoupper($unauthMethod);
}
public function getAuthRequired() {
return ['authRequiredLevel'=>$this->authRequiredLevel,'unauthUrl'=>$this->unauthUrl,'unauthMethod'=>$this->unauthMethod];
}
public function setAuthUser($auth) {
$this->authValue=$auth;
}
public function getAuthUser() {
return $this->authValue;
}
public function run() {
if($this->lastEndpoint[0] && $this->lastEndpoint[0]===$this->uri && $this->lastEndpoint[1]===$this->method) {
throw \ErrorException("Recursive call to same endpoint");
}
foreach ($this->routes[$this->method] as $pattern => $settings) {
$pattern="#$pattern#";
if (preg_match($pattern, $this->uri, $params) === 1) {
syslog(LOG_INFO,"Router Run Match: $params[0] $this->method");
if($this->authValue<$settings['authRequiredLevel']) {
$this->lastEndpoint=[$this->uri,$this->method];
$this->uri=$settings['unauthUrl'];
$this->method=$settings['unauthMethod'];
syslog(LOG_INFO,"Unauthorized. Instead do: $this->method $this->uri");
return $this->run();
}
return call_user_func($settings['callback'], new Request($params), new Response($this->container['template']))->render();
}
}
return $this->container['missingErrorHandler'](new Request(), new Response($this->container['template']));
}
}
<?php
namespace Michael\SimpleRouter;
class Request implements RequestInterface {
private $params = [];
private $data=false;
private $uri, $method;
public function __construct(array $params=null)
{
if(is_null($params)) $params=[$_SERVER['REQUEST_URI']];
$this->uri=$params[0];
array_shift($params);
$this->params=array_values($params);
$this->method=$_SERVER['REQUEST_METHOD'];
}
public function getMethod() {
return $this->method;
}
public function getUri() {
return $this->uri;
}
public function getParams() {
return $this->params;
}
public function getData() {
if ($this->data!==false) return $this->data;
switch($this->method){
case 'GET':$this->data=$_GET;break;
case 'POST':$this->data=$_POST;break;
case 'PUT':
case 'DELETE': //Can a delete method have data? Is it the same as $_GET or PUT?
parse_str(file_get_contents("php://input"),$this->data);
/*
//Or do it per http://php.net/manual/en/features.file-upload.put-method.php?
$putdata = fopen("php://input", "r");
// Read the data 1 KB at a time and write to a stream
while ($data = fread($putdata, 1024)) {
fwrite($fp, $data);
}
fclose($fp);
*/
break;
}
return $this->data;
}
}
<?php
namespace Michael\SimpleRouter;
class Response implements ResponseInterface {
private $folder;
private $buffer=null;
public function __construct($folder)
{
$this->folder=$folder;
}
public function render() {
return $this->buffer;
}
public function html($template, $arr, $code=200) {
if($code!==200) http_response_code($code);
$template=file_get_contents("$this->folder/$template");
$this->buffer=$arr?$this->substitute($template,$arr):$template;
//or $this->buffer=$this->parse($template,$arr);
return $this;
}
public function json($data, $code=200) {
header('Content-Type: application/json');
http_response_code($code);
$this->buffer=$data?json_encode($data):null;
return $this;
}
public function error($msg,$code)
{
return $this->isAjax()
?$this->json(['error'=>$msg],404)->render()
:$this->html('error.html',['errorMessage'=>$msg],404)->render();
}
public function redirect($location)
{
header("Location: $location");
return $this;
}
private function isAjax()
{
return isset($_SERVER['HTTP_X_REQUESTED_WITH']) && !empty($_SERVER['HTTP_X_REQUESTED_WITH']) && strtolower($_SERVER['HTTP_X_REQUESTED_WITH']) == 'xmlhttprequest';
}
private function substitute($template, $values, $deliminator='{{}}',$preface=null) {
$length=strlen($deliminator)/2;
$open = substr($deliminator, 0, $length);
$close = substr($deliminator, $length);
foreach ($values as $key => $data) {
if(is_array($data)){
$template=$this->substitute($template,$data,$deliminator,$key.'.');
}
else {
$template = str_replace($open.$preface.$key.$close, $data, $template);
}
}
return $template;
}
private function parse($template, $values) {
ob_start();
require("$this->folder/$template.html");
$template = ob_get_contents();
ob_end_clean();
return $template;
}
}
<?php
namespace Michael\EmbeddedWebserver\Controllers;
class Logon implements LogonInterface {
public function validCredentials()
{
return isset($_POST['username'],$_POST['password']) && $_POST['username']=='michael' && $_POST['password']=='mypassword';
}
public function setAuthorityLevel($auth)
{
$_SESSION['authorized']=$auth;
}
public function getAuthorityLevel()
{
return isset($_SESSION['authorized'])?$_SESSION['authorized']:0;
}
}
<?php
namespace Michael\EmbeddedWebserver\Controllers;
class Page implements PageInterface {
public function getData($request)
{
return [
'method'=>$request->getMethod(),
'uri'=>$request->getUri(),
'params'=>json_encode($request->getParams()),
'data'=>json_encode($request->getData()),
];
}
}