Skip to content

Commit

Permalink
Introducing @before and @after annotations to
Browse files Browse the repository at this point in the history
handle pre- and post-routing
  • Loading branch information
shaggy8871 committed Dec 30, 2016
1 parent d64460d commit 80ea3ee
Show file tree
Hide file tree
Showing 5 changed files with 248 additions and 0 deletions.
27 changes: 27 additions & 0 deletions src/Frame/Core/Caller.php
Original file line number Diff line number Diff line change
Expand Up @@ -39,4 +39,31 @@ public function __get($property)

}

/*
* Activate getProperty() style method calls
*/
public function __call($name, $args)
{

if (substr($name, 0, 3) == 'get') {
$property = lcfirst(substr($name, 3));
if (property_exists($this, $property)) {
return $this->$property;
} else {
throw new Exception\UnknownPropertyException($property, __CLASS__);
}
}

}

/*
* Returns true if the property exists
*/
public function __isset($property)
{

return property_exists($this, $property);

}

}
115 changes: 115 additions & 0 deletions src/Frame/Core/Router.php
Original file line number Diff line number Diff line change
Expand Up @@ -275,6 +275,8 @@ private function invokeFunction($function)
private function invokeCallable($reflection, $class = null)
{

$this->beforeCall($reflection, $class);

// Get an array of ReflectionParameter objects
$params = $reflection->getParameters();
// Injection array
Expand Down Expand Up @@ -332,6 +334,8 @@ private function invokeCallable($reflection, $class = null)
$response = $reflection->invokeArgs($inject);
}

$this->afterCall($response);

if (($response !== false) && ($response !== null)) {
// If object is a Response class, simply call the render method (assume it knows what to do)
// Otherwise call the render method on the defined/default response class
Expand All @@ -357,6 +361,117 @@ private function invokeCallable($reflection, $class = null)

}

/**
* Allows a @before annotation to determine a different route
*/
private function beforeCall(&$reflection, &$class)
{

if (!isset($this->caller->annotations['before'])) {
return; // proceed
}

$route = $this->caller->annotations['before'];
$projectControllers = $this->project->ns . '\\Controllers\\';
$beforeClass = null;
$beforeReflection = null;

// If we get a string back in format $controller::$method, look for the method
// If the return class method starts with "\" char, look outside the project controller tree
if ((is_string($route)) && (strpos($route, '::') !== false)) {
list($controller, $method) = explode('::', ($route[0] != '\\' ? $projectControllers : '') . $route);
if ((class_exists($controller)) && (is_callable($controller . '::' . $method, true))) {
$beforeClass = new $controller($this->project);
$beforeReflection = new \ReflectionMethod($beforeClass, $method);
}
} else
// If we get a method name back, look in the same class
if ((is_string($route)) && (method_exists($class, $route))) {
$beforeClass = $class;
$beforeReflection = new \ReflectionMethod($class, $route);
} else
// Otherwise if it's callable, it must be a function
if (is_callable($route)) {
$beforeClass = $class;
$beforeReflection = new \ReflectionFunction($route);
}

if (!($beforeReflection instanceof \ReflectionFunctionAbstract)) {
return; // ignore; @todo: log reason
}

// Get an array of ReflectionParameter objects
$params = $beforeReflection->getParameters();
// Injection array
$inject = [];
// Loop through parameters to determine their class types
foreach($params as $param) {
try {
$paramClass = $param->getClass();
} catch (\Exception $e) {
// Rethrow the error with further information
throw new ClassNotFoundException($param->getName(), ($this->caller->controller ? get_class($this->caller->controller) : null), $this->caller->method);
}
// If it's not a class, inject a null value
if (!($paramClass instanceof \ReflectionClass)) {
$inject[] = null;
continue;
}
// Special case for a Url and Project type hints, send in the one we already have
if ($paramClass->name == 'Frame\\Core\\Url') {
$inject[] = $this->url;
} else
if ($paramClass->name == 'Frame\\Core\\Project') {
$inject[] = $this->project;
} else
if ($paramClass->name == 'Frame\\Core\\Context') {
$inject[] = new Context($this->project, $this->url, $this->caller);
} else {
$inject[] = null;
}
}

// Send the injected parameters into the identified method
if ($beforeReflection instanceof \ReflectionMethod) {
$response = $beforeReflection->invokeArgs($beforeClass, $inject);
} else {
$response = $beforeReflection->invokeArgs($inject);
}

// If we get a string back in format $controller::$method, look for the method
// If the return class method starts with "\" char, look outside the project controller tree
if ((is_string($response)) && (strpos($response, '::') !== false)) {
list($controller, $method) = explode('::', ($response[0] != '\\' ? $projectControllers : '') . $response);
if ((class_exists($controller)) && (is_callable($controller . '::' . $method, true))) {
// Override parameters:
$class = new $controller($this->project);
$reflection = new \ReflectionMethod($class, $method);
}
} else
// If we get a method name back, look in the same class
if ((is_string($response)) && (method_exists($class, $response))) {
$reflection = new \ReflectionMethod($class, $response);
} else
// Otherwise, if we get a closure back, call it
if (is_callable($response)) {
if ((is_array($response)) && (count($response) == 2)) {
// Override parameters:
$class = new $response[0];
$reflection = new \ReflectionMethod($response[0], $response[1]);
} else {
$reflection = new \ReflectionFunction($response);
}
}

}

/*
* @todo
*/
private function afterCall(&$response)
{
}

/*
* Returns true if it's a Request class
*/
Expand Down
72 changes: 72 additions & 0 deletions tests/Frame/Tests/Controllers/Auth.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
<?php

namespace Frame\Tests\Controllers;

use Frame\Core\Controller;
use Frame\Core\Url;
use Frame\Core\Context;
use Frame\Request\RouteParams;

class Auth extends Controller
{

/**
* @before authUser
*/
public function routeAllowed()
{

return "routeAuthOkay";

}

/**
* @before authUser
*/
public function routeNotAllowed()
{

return "routeAuthShouldNotSeeThis";

}

/**
* @before authUser
*/
public function routeNotAllowedOutside()
{

return "routeAuthShouldNotSeeThis";

}

public function routeNotAuthorized()
{

return "routeNotAuthorized";

}

/**
* Authorizes the user
*/
public function authUser(Url $url, Context $context)
{

// One method:
switch($url->getRequestUri()) {
case '/auth/notallowed':
return 'routeNotAuthorized';
}

// Another method:
switch($context->getCaller()->getMethod()) {
case 'routeNotallowedoutside': // Note case change!
return 'Index::routeNotAuthorized';
}

// No response allows the rest through

}

}
7 changes: 7 additions & 0 deletions tests/Frame/Tests/Controllers/Index.php
Original file line number Diff line number Diff line change
Expand Up @@ -224,4 +224,11 @@ public function routeProducts()

}

public function routeNotAuthorized()
{

echo 'routeIndexNotAuthorized';

}

}
27 changes: 27 additions & 0 deletions tests/Frame/Tests/RouterTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -318,6 +318,33 @@ public function testModelsInstantiateRequest()

}

public function testAuthAllowed()
{

$this->expectOutputString('routeAuthOkay');

$this->router->parseUrl($this->generateUrl('/auth/allowed'));

}

public function testAuthNotAllowed()
{

$this->expectOutputString('routeNotAuthorized');

$this->router->parseUrl($this->generateUrl('/auth/notallowed'));

}

public function testAuthNotAllowedOutside()
{

$this->expectOutputString('routeIndexNotAuthorized');

$this->router->parseUrl($this->generateUrl('/auth/notallowedoutside'));

}

public static function setUpBeforeClass()
{

Expand Down

0 comments on commit 80ea3ee

Please sign in to comment.