I'm trying to implement Simple MVC Framework http://simplemvcframework.com/ and am going through the code line by line in the index.php file (https://github.com/simple-mvc-framework/v2/blob/master/index.php) and I've come across the following 2 lines..
//define routes
Router::any('', '\controllers\welcome#index');
Router::any('/subpage', '\controllers\welcome#subpage');
I understand :: is a scope resolution operator, and would think that Router::any() would be referencing a static method called any() in the Router class... however no such method exists... https://github.com/simple-mvc-framework/v2/blob/master/app/core/router.php. Though all other static method calls mentioned in the index.php file DO exist.
I thought maybe this was some sort of reserved name of a PHP function, but of course as you could imagine, searching for "PHP Any function" or similar searches in google doesn't come back with too many helpful results. My other thought is maybe this is just a implementation of static calls that I'm not familiar with?
I know this is a very specific question, but I'm trying to make sure I understand as much as possible with this framework and PHP in general before going too much futher.
Here is how it works.
It utilizes __callStatic magic method in PHP.
When a static call is made using Class/Object, and if the magic method is defined in the class, and if the called static function doesn't exist then this method is invoked.
If we dig deeper into the code,
public static function __callstatic($method, $params){
$uri = dirname($_SERVER['PHP_SELF']).'/'.$params[0];
$callback = $params[1];
array_push(self::$routes, $uri);
array_push(self::$methods, strtoupper($method));
array_push(self::$callbacks, $callback);
}
The method parameter which is any in our case is store as uppercase (ANY) with a callback.
When a request is made, dispatch function is called.
public static function dispatch(){
$uri = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH);
$method = $_SERVER['REQUEST_METHOD'];
$searches = array_keys(static::$patterns);
$replaces = array_values(static::$patterns);
self::$routes = str_replace('//','/',self::$routes);
$found_route = false;
// parse query parameters
{
$query = '';
$q_arr = array();
if(strpos($uri, '&') > 0) {
$query = substr($uri, strpos($uri, '&') + 1);
$uri = substr($uri, 0, strpos($uri, '&'));
$q_arr = explode('&', $query);
foreach($q_arr as $q) {
$qobj = explode('=', $q);
$q_arr[] = array($qobj[0] => $qobj[1]);
if(!isset($_GET[$qobj[0]]))
{
$_GET[$qobj[0]] = $qobj[1];
}
}
}
}
// check if route is defined without regex
if (in_array($uri, self::$routes)) {
$route_pos = array_keys(self::$routes, $uri);
// foreach route position
foreach ($route_pos as $route) {
if (self::$methods[$route] == $method || self::$methods[$route] == 'ANY') {
$found_route = true;
//if route is not an object
if(!is_object(self::$callbacks[$route])){
//call object controller and method
self::invokeObject(self::$callbacks[$route]);
if (self::$halts) return;
} else {
//call closure
call_user_func(self::$callbacks[$route]);
if (self::$halts) return;
}
}
}
// end foreach
} else {
// check if defined with regex
$pos = 0;
// foreach routes
foreach (self::$routes as $route) {
$route = str_replace('//','/',$route);
if (strpos($route, ':') !== false) {
$route = str_replace($searches, $replaces, $route);
}
if (preg_match('#^' . $route . '$#', $uri, $matched)) {
if (self::$methods[$pos] == $method || self::$methods[$pos] == 'ANY') {
$found_route = true;
//remove $matched[0] as [1] is the first parameter.
array_shift($matched);
if(!is_object(self::$callbacks[$pos])){
//call object controller and method
self::invokeObject(self::$callbacks[$pos],$matched);
if (self::$halts) return;
} else {
//call closure
call_user_func_array(self::$callbacks[$pos], $matched);
if (self::$halts) return;
}
}
}
$pos++;
}
// end foreach
}
if (self::$fallback) {
//call the auto dispatch method
$found_route = self::autoDispatch();
}
// run the error callback if the route was not found
if (!$found_route) {
if (!self::$error_callback) {
self::$error_callback = function() {
header("{$_SERVER['SERVER_PROTOCOL']} 404 Not Found");
echo '404';
};
}
if(!is_object(self::$error_callback)){
//call object controller and method
self::invokeObject(self::$error_callback,null,'No routes found.');
if (self::$halts) return;
} else {
call_user_func(self::$error_callback);
if (self::$halts) return;
}
}
}
}
If you look deeply into the dispatch function, you will clearly see that, there are several lines containing:
if (self::$methods[$route] == $method || self::$methods[$route] == 'ANY')
This helps routing the request to defined callbacks based on the methods supplied including ANY method.
It's not that any() is a reserved method, it's that the class is using overloading to call that method. Look at this code for a second
/**
* Defines a route w/ callback and method
*
* #param string $method
* #param array #params
*/
public static function __callstatic($method, $params){
$uri = dirname($_SERVER['PHP_SELF']).'/'.$params[0];
$callback = $params[1];
array_push(self::$routes, $uri);
array_push(self::$methods, strtoupper($method));
array_push(self::$callbacks, $callback);
}
When any() is called, PHP first checks for that method being defined directly. Since it's not, it then calls this overloading magic method, which then executes the call.
Related
I am trying to load (include file) the GetRiskSummaryCommandHandler.php (GetRiskSummary\CommandHandler) at runtime dynamically, while resolving Route api/risks to HandleCommand (of class CommandHandler) method.
How can I do this? If not can I modify any taken approach including modifying autoloader?
My api class snippet looks like this:
API.php
<?php
abstract class API
{
public function processRequest()
{
$id1 = $this->requestObj->id1;
//$id2 = $this->requestObj->id2;
$endpoint1 = $this->requestObj->endpoint1;
$endpoint2 = $this->requestObj->endpoint2;
$isDestination = in_array($id1, ['first', 'prev', 'next', 'last']);
$numSetEndpoints = (int)isset($endpoint1) + (int)isset($endpoint2);
switch($numSetEndpoints)
{
case 0:
if ($isDestination)
return json_decode($this->_response("No Endpoint: ", $endpoint1));
return json_decode($this->_response("ProjectAIM API"));
case 1:
$className = $endpoint1.'Controller';
break;
case 2:
$className = $endpoint2.'Controller';
break;
}
$class = "GetRiskSummaryCommandHandler";
$method = "HandleCommand";
if (class_exists($class))
{
if (method_exists($class, $method))
{
$response = (new $class($this->requestObj))->{$method}($this->requestObj);
if ($response['Succeeded'] == false)
{
return $response['Result'];
}
else if ($response['Succeeded'] == true)
{
header("Content-Type: application/json");
return $this->_response($response);
}
else if ($response['Result'])
{
header("Content-Type: text/html");
return $this->_response($response);
}
}
}
}
**Command Handler Snippet, uses a Route Attiribute
GetRiskSummaryCommandHandler.php
<?php
namespace GetRiskSummary;
use Infrastructure\CommandHandler;
class GetRiskSummaryCommandHandler extends CommandHandler
{
#[Route("/api/risks", methods: ["GET"])]
public function HandleCommand()
{
$qb = $this->getEntityManager()
->createQueryBuilder();
$qb->select('*')
->from('Risks', 'Risks')
->orderBy('RiskID', 'DESC');
$query = $qb->getQuery();
return $query->getResult();
}
}
Autoloader.php
<?php
namespace Autoloader;
class Autoloader
{
private static function rglob($pattern, $flags = 0) {
$files = glob($pattern, $flags);
foreach (glob(dirname($pattern).'/*', GLOB_ONLYDIR|GLOB_NOSORT) as $dir) {
$files = array_merge($files, self::rglob($dir.'/'.basename($pattern), $flags));
}
return $files;
}
public static function ClassLoader($path)
{
$pathParts = explode("\\", $path);
$path = $pathParts[array_key_last($pathParts)];
$matches = self::rglob("*/$path*");
foreach ($matches as $name)
{
$filePath = realpath($name);
if (file_exists($filePath))
include $filePath;
}
}
}
spl_autoload_register("AutoLoader\AutoLoader::ClassLoader");
I think the biggest missing feature of PHP Attributes would be that as far as I'm aware PHP Attributes are static and aren't evaluated at runtime. They are just glorified comments.
If it was true, we could do some truly amazing things like non-intrusively attaching pre/post-processing functions, just like what you wrote.
They work just like in symfony or doctrine. You can't use variables in them and you must write your own code to search your php class tree for attributes in the code and use php reflection to obtain the information and run the attributes.
To be honest it's pretty disappointing, although maybe it's just the first step that will be fixed in later versions of PHP.
But for now, I think what you're asking for is impossible. Because they are not running dynamically like you appear to be asking.
I am building simple php route But I
I have the following, which is working
Route::add('/', function(){
echo 'Hello world message';
});
My challange is that... I want to be able to load the function directly from a controller which I am calling
use Controllers\LoginController;
Route::add('/', LoginController::login());
the login() is a function i created inside LoginController class
I get this error
<b>Warning</b>: call_user_func_array() expects parameter 1 to be a valid callback, no array or string given in <b>C:\xampp\htdocs\Config\Route.php</b> on line <b>75</b><br />
My Route.php
<?php
namespace Config;
class Route{ private static $routes = Array(); private static $pathNotFound = null; private static $methodNotAllowed = null;
public static function add($expression, $function, $method = 'get') {
array_push(self::$routes,Array(
'expression' => $expression,
'function' => $function,
'method' => $method
)); }
public static function pathNotFound($function){
self::$pathNotFound = $function; }
public static function methodNotAllowed($function){
self::$methodNotAllowed = $function; }
public static function run($basepath = '/')
{
// Parse current url
$parsed_url = parse_url($_SERVER['REQUEST_URI']);//Parse Uri
if(isset($parsed_url['path'])){
$path = $parsed_url['path'];
}else{
$path = '/';
}
// Get current request method
$method = $_SERVER['REQUEST_METHOD'];
$path_match_found = false;
$route_match_found = false;
foreach(self::$routes as $route){
// If the method matches check the path
// Add basepath to matching string
if($basepath!=''&&$basepath!='/'){
$route['expression'] = '('.$basepath.')'.$route['expression'];
}
// Add 'find string start' automatically
//$route['expression'] = '^'.$route['expression'];
// Add 'find string end' automatically
$route['expression'] = $route['expression'].'$';
//echo $path.'<br/>';
// Check path match
if(preg_match('#'.$route['expression'].'#',$path,$matches)){
$path_match_found = true;
// Check method match
if(strtolower($method) == strtolower($route['method'])){
array_shift($matches);// Always remove first element. This contains the whole string
if($basepath!=''&&$basepath!='/'){
array_shift($matches);// Remove basepath
}
call_user_func_array($route['function'], $matches);
$route_match_found = true;
// Do not check other routes
break;
}
}
}
// No matching route was found
if(!$route_match_found){
// But a matching path exists
if($path_match_found){
header("HTTP/1.0 405 Method Not Allowed");
if(self::$methodNotAllowed){
call_user_func_array(self::$methodNotAllowed, Array($path,$method));
}
}else{
header("HTTP/1.0 404 Not Found");
if(self::$pathNotFound){
call_user_func_array(self::$pathNotFound, Array($path));
}
}
}
}
}
Is there anyway i can make use of external function inside the route?
It wants a callback, so you need to pass a function itself. In your case using the parenthesis () invokes (calls) the function and returns the result of it instead of passing the function itself.
So here is a simple example of how you could do it:
Route::add('/', LoginController::login);
Wrapping it into a another function enables you to even pass arguments.
// shorthand function syntax since 7.4
Route::add('/', fn() => LoginController::login());
// example arguments (just a prototype)
Route::add('/', fn() => LoginController::login($username, $password));
or
Route::add('/', function() { LoginController::login() });
// example arguments (just a prototype)
Route::add('/', function() { LoginController::login($username, $password) });
I'm having a few problems calling a function from a string, as its a lot more complicated than just calling a function, I need to do it inside another class, with another namespace.
I dispatch my routes using this method, I am using TreeRoute package on GitHub.
public function dispatch() {
$uri = substr($_SERVER['REQUEST_URI'], strlen(implode('/', array_slice(explode('/', $_SERVER['SCRIPT_NAME']), 0, -1)) . '/'));
$method = $_SERVER['REQUEST_METHOD'];
$result = $this->router->dispatch($method, $uri);
if (!isset($result['error'])) {
$handler = $result['handler'];
$params = $result['params'];
// TODO: Call the handler here, but how?
}
else {
switch ($result['error']['code']) {
case 404 :
echo 'Not found handler here';
break;
case 405 :
echo 'Method not allowed handler here';
$allowedMethods = $result['allowed'];
if ($method == 'OPTIONS') {
// OPTIONS method handler here
}
break;
}
}
}
I register routes like this:
public function __construct() {
$this->router = new \TreeRoute\Router();
}
public function setRoutes() {
$this->router->addRoute('GET', '/test', 'App/Controllers/SomeController/test');
}
What I want to do is call function 'test' in class 'SomeController', now 'SomeController has a namespace of 'App\Controllers'.
I looked into calluserfunc but I couldn't work out how to do it with my string format and with a namespace, can someone help me out?
http://php.net/manual/en/function.call-user-func.php
You can call call_user_func with full class path like this:
call_user_func('App\Controllers\SomeController::test', $arg1, $arg2)
// or
call_user_func(['App\Controllers\SomeController', 'test'], $arg1, $arg2)
// or better
use App\Controllers\SomeController;
call_user_func([SomeController::class, 'test'], $arg1, $args2);
I believe this will do the job
if (!isset($result['error'])) {
$handler = $result['handler'];
$params = $result['params'];
$method = substr( strrchr($handler, '/'), 1 ); // get the method's name from $handler string
$class = substr($handler, 0, -strlen($method)-1); // get the class name from $handler string
$class = str_replace('/', '\\', $class); // change slashes for anti-slashes
// then choose one of those :
// if your methods are static :
call_user_func_array([$class, $method], $params);
// otherwise :
(new $class)->{$method}(...$params); // needs php 7
}
Is there any way to use a variable of a method, after a recursive call (without sending that as a parameter).
For example:
class Doc {
public function readDoc($file, $recursion = false) {
if ($recursion != false) {
// DO SOMETHING;
}
$filename = $file."Some added text";
$this->readDoc($filename, 1);
}
}
Here, is it possible to use the value of $file sent in the first call (when the readDoc() function is called recursively).
You can create a simple stack with an array, e.g.
class Doc {
private $stack = [];
public function readDoc($file, $recursion=false) {
if($recursion != false)
DO SOMETHING
$this->stack[] = $file;
$filename = $file."Some added text";
$this->readDoc($filename, 1);
}
}
And then get the first index of the array as your $file variable.
you could also use an anonymous function to work in a different scope something like this:
public function test($file, $recursion = false)
{
$rec = function($r) use ($file, &$rec)
{
if($r !== false) {
}
$filename = $file.'test';
return $rec($r);
};
return $rec($recursion);
}
in this case the $file variable always stays the same
(be aware that the above example creates an infinite-loop)
For accessing previous URL in laravel. I am using this code in my controller.
$current_url = Request::url();
$back_url = redirect()->back()->getTargetUrl();
if ($current_url != $back_url) {
Session::put('previous_url', redirect()->back()->getTargetUrl());
}
This method helps maintainging previous url even when server side validation fails.
In my blade I access previous url like this {{ Session::get('previous_url') }}.
I need to find the second segment of my previous url.
Thanks
You can do it this way:
request()->segment(2);
request() is a helper function that returns Illuminate\Http\Request, but you can also use the facade Request or inject the class as a dependency in your method.
EDIT
with the redirect back: redirect()->back()->getRequest()->segment(2);
Under the hood, Laravel is doing these two things to get the segments of a url (from within the Request class):
public function segment($index, $default = null)
{
return Arr::get($this->segments(), $index - 1, $default);
}
public function segments()
{
$segments = explode('/', $this->path());
return array_values(array_filter($segments, function ($v) {
return $v != '';
}));
}
You could do something similar in a helper function:
public function segment($url, $index, $default)
{
return Arr::get($this->segments($url), $index - 1, $default);
}
public function segments($url)
{
$segments = explode('/', $url);
return array_values(array_filter($segments, function ($v) {
return $v != '';
}));
}