Dynamically call a method from a dynamically called class - php

I'm trying to call a controller and method pulled from the URI site.com/controller/method
Here is my current working code:
$__REQUEST__ = new URI_Request($_SERVER["REQUEST_URI"]);
$this->prepareController($__REQUEST__);
if($this->checkClass()) {
$this->controller = new $this->controller();
if($this->checkMethod($__REQUEST__)) {
$method = $__REQUEST__->getMethod();
$this->method = $method;
$this->controller->$method();
}
}
However, I want this line
$this->controller->$method();
To work as similarly to this
$this->controller = new $this->controller();
//achieves something like $this->controller = new IndexController();
//if the URL was something like site.com/index/test (/index/ gets manipulated)
i.e. something like
$this->controller->$this->method
I can see why this wouldn't work, however - is there a way to chain this or get it to reference the $method variable from the object characteristics rather than a stray variable?

$this->controller->{$this->method}();

Related

PHP Calling dynamic functions

Im trying to figure out how to call functions based on what a user clicks on a form. But im not sure if im doing it right.
I have a number of classes, lets say 3 for different ways to connect to a site, the user clicks on which one they would like.
FTP
SFTP
SSH
Which i have named 'service' in my code.
I don't want to run a whole bunch of IF statements, i would rather try and build the call dynamically.
What i have at the moment is as follows
$ftp_backup = new FTPBackup;
$sftp_backup = new SFTPBackup;
$ssh_backup = new SSHBackup;
$service = $request->input('service') . '_backup';
$service->testConn($request);
Im getting the following error
Call to a member function testConn() on string
Im not sure im doing this right.
Any help would be greatly appreciated.
Thanks
First of all $service is a string on which You cannot call method, because it is not an object (class instance).
I think it is a great example of where You can use Strategy Pattern which look like that:
class BackupStrategy {
private $strategy = null;
public function __construct($service_name)
{
switch ($service_name) {
case "ftp":
$this->strategy = new FTPBackup();
break;
case "sftp":
$this->strategy = new SFTPBackup();
break;
case "ssh":
$this->strategy = new SSHBackup();
break;
}
}
public function testConn()
{
return $this->strategy->testConn();
}
}
And then in place where You want to call it You call it by:
$service = new BackupStrategy($request->input('service'));
$service->testConn($request);
I suggest You to read about Design Patterns in OOP - it will help You a lot in the future.
How about this:
$ftp_backup = new FTPBackup;
$sftp_backup = new SFTPBackup;
$ssh_backup = new SSHBackup;
$service = $request->input('service') . '_backup';
${$service}->testConn($request);
This is called "Variables variable": http://php.net/manual/en/language.variables.variable.php
// Create class name
$className = $request->get('service') . '_backup';
// Create class instance
$service = new $className();
// Use it as you want
$service->testConn($request);

Set a referenced variable to a newly initialized class

I have a method, which takes a reference
// CarService.php
public function getCars(&$carCollection = null)
{
$promise = // guzzle request for getting all cars would be here
$promise->then(function (ResponseInterface $response) use (&$carCollection) {
$cars= json_decode($response->getBody(), true);
$carCollection= new CarCollection($cars);
});
}
However, when accessing the collection and trying to reuse it, I'm getting the error
Argument 1 passed to {placeholder} must be an instance of {placeholder}, null given
I know that the reason for this is, that the constructor returns nothing, but how can I still assign my variable to a new instance of the CarCollection (which extends Doctrine's ArrayCollection)
I even tried it with a static method as a work around
// CarCollection.php
public static function create(array $cars): CarCollection
{
$carCollection = new CarCollection($cars);
return $carCollection;
}
// CarService.php
public function getCars(&$carCollection = null)
{
$cars = // curl request for getting all cars would be here
$carCollection = CarCollection::create($cars)
}
but it's still null. Why is that? How can I set a referenced variable to a new class?
I access the method like this
$carService = $this->get('tzfrs.vehicle.services.car');
$carCollection = null;
$promises = [
$carService->getCars($carCollection)
];
\GuzzleHttp\Promise\unwrap($promises);
var_dump($carCollection); // null
When I set the reference directly, eg.
// CarService.php
public function getCars(&$carCollection = null)
{
$carCollection = new CarCollection([]);
}
it works without any problems. Seems like the callback is somehow the problem.
Whoever downvoted this, can you please elaborate why and why you voted to close?
I might be misunderstanding the question, but you should be able to modify an object when passing by reference. See here for an example: https://3v4l.org/KtFvZ
In the later example code that you added, you shouldn't pass $carCollection by reference, the & should only be in the method/function defintion, not provided when you call it. I don't think that is your problem though, that should be throwing an error in php7.

Dynamically create a new object from a class with different namespace

I'm trying to create a small RESTful API for my database and I encountered a problem creating controller object dynamically based on user request, because all my code is using namespaces and just doing:
$api = new $controllerName($request);
Won't work. Because $controllerName would resolve to "ReadController", but is actually \controllers\lottery\ReadController hence the error
The whole part of defining the path to the class is:
if ($method === 'GET') {
$controllerName = 'ReadController';
// #NOTE: $category is a part of $_GET parameters, e.g: /api/lottery <- lottery is a $category
$controllerFile = CONTROLLERS.$category.'/'.$controllerName.'.php';
if (file_exists($controllerFile)) {
include_once($controllerFile);
$api = new $controllerName($request);
} else {
throw new \Exception('Undefined controller');
}
}
And the declaration of ReadController in core\controllers\lottery\ReadController.php
namespace controllers\lottery;
class ReadController extends \core\API {
}
Any ideas how to dynamically create the object?
Thanks!
$controllerName = 'controllers\lottery\ReadController';
new $controllerName($request);
Classes instantiated from strings must always use the fully qualified class name.

how to build a good router for php mvc

I'm experimenting with php mvc and I'm stucked with the following issue. My request and router classes are really simple and I would like to extend theme to can handle controller calls from sub folders and to controller classes functions should be able to pick up url variables send it threw get and post.
my router looks as it follows
class Router{
public static function route(Request $request){
$controller = $request->getController().'Controller';
$method = $request->getMethod();
$args = $request->getArgs();
$controllerFile = __SITE_PATH.'/controllers/'.$controller.'.php';
if(is_readable($controllerFile)){
require_once $controllerFile;
$controller = new $controller;
if(!empty($args)){
call_user_func_array(array($controller,$method),$args);
}else{
call_user_func(array($controller,$method));
}
return;
}
throw new Exception('404 - '.$request->getController().'--Controller not found');
}
}
and Request class
private $_controller;
private $_method;
private $_args;
public function __construct(){
$parts = explode('/',$_SERVER['REQUEST_URI']);
$this->_controller = ($c = array_shift($parts))? $c: 'index';
$this->_method = ($c = array_shift($parts))? $c: 'index';
$this->_args = (isset($parts[0])) ? $parts : array();
}
public function getController(){
return $this->_controller;
}
public function getMethod(){
return $this->_method;
}
public function getArgs(){
return $this->_args;
}
}
The problem is:when I try to send threw ajax, variables to a controller method this are not recognized because of its url structure.
For example
index/ajax?mod_title=shop+marks&domain=example
is accepted just if it look
index/ajax/shop+mark/example
Your code contains what is known as an LFI vulnerability and is dangerous in its current state.
You should whitelist your what can be used as your $controller, as otherwise an attacker could try to specify something using NUL bytes and possibly going up a directory to include files that SHOULD NOT be ever included, such as /etc/passwd, a config file, whatever.
Your router is not safe for use; beware!
edit: example on whitelisting
$safe = array(
'ajax',
'somecontroller',
'foo',
'bar',
);
if(!in_array($this->_controller, $safe))
{
throw new Exception(); // replace me with your own error 404 stuff
}
Since your Request class uses a URI segments approach for identifying controller, action and arguments, global variables such as $_GET or $_REQUEST are not taken into account from within your Request.
What you need to do is to make some additions to your Request code. Specifically:
Remove the line:
$this->_args = (isset($parts[0])) ? $parts : array();
And add the following:
$all_parts = (isset($parts[0])) ? $parts : array();
$all_parts['get'] = $_GET;
$this->_args = $all_parts;
This way, $_GET (ie variables passed via the url) variables will be available in the actions called, as they will be in $args (they will be available as $args['get'] actually, which is the array that holds the $_GET vars, so you will be able to have access to domain=example by using $args['get']['domain']).
Ofcourse, you can add one more method in your Request class (e.g. query) that might look like that:
public function query($var = null)
{
if ($var === null)
{
return $_GET;
}
if ( ! isset($_GET[$var]) )
{
return FALSE;
}
return $_GET[$var];
}
This way, you can get a single variable from the url (e.g. $request->query('domain')) or the whole $_GET array ($request->query()).
That's because php will put "?mod_title=..." in the $_GET array automatically. Your getArgs() function should check for $_GET, $_POST or $_REQUEST.
If you're trying for a minimal MVC approach, have a look at rasmus' example: http://toys.lerdorf.com/archives/38-The-no-framework-PHP-MVC-framework.html
If your use case is going to get more complex, have a look at how Zend (http://framework.zend.com/manual/en/zend.controller.html) or Symfony (https://github.com/symfony/symfony/tree/master/src/Symfony/Component/Routing) do their stuff.
Choose any popular MVC to see how they implement it under the hood. In addition, spl_autoload_register and namespace are your friends.

PHP call_user_func alternative

I am looking for a pretty equivalent in PHP to its function call_user_func.
The problem I am encountering with this function is that it does not go into an "object mode". By this, I mean I cannot use $this and other stuff in the class, so pretty much in two words: not oop.
I need this basically as I am dealing with the requested url, parsing it, and seeing if everything is ok and so on, and then doing the following lines:
call_user_func(array(ucfirst( $controller . "Controller" ), '_initAction'), $param);
call_user_func(array(ucfirst( $controller . "Controller" ), $action . 'Action'), $param);
as I want to dynamically call the "Controller" and its actions. But I cannot use $this in the $action methods as it is not OOP.
Here the message I get:
Fatal error: Using $this when not in object context in E:\htdocs\wit\application\controller\InformationController.php on line 6
So I hope that somebody could help me.
Could you also tell me if I am approaching this problem in a wrong way?
PS: Please don't recommend me any MVC frameworks that take care of this stuff. I like Zend, but sometimes, its just too heavy :(( I need a lightweight setup for this.
You can call the object method by passing object in first element of the callback:
$class = ucfirst($controller . "Controller");
$controller = new $class();
call_user_func(array($controller, $action . 'Action'), $param);
Actually, you can even use
$controller = new $class();
$controller->{$action . 'Action'}();
Take a look at how Glue calls user defined functions. It may point you in the right direction, since users define classes to handle routes.
<?php
require_once('glue.php');
$urls = array(
'/' => 'index'
);
class index {
function GET() {
echo "Hello, World!";
}
}
glue::stick($urls);
?>
If I completely miss the point of your question, I'm sorry. It sounds to me like you're trying to call an object method using call_user_func. You can do that, you just pass an array with the object as the first index, and the string name of the method as the second index.
For example, say you have your controller "IndexController" with "index" as your action/method.
class IndexController {
public function index() {
// $this available here
}
}
$controller = new IndexController();
// if you know your parameters all ahead of time
call_user_func(array($controller, 'index'), $param1, $param2);
// if you want to pass an unknown number of params
call_user_func_array(array($controller, 'index'), $params);
$view = View::getInstance( $config['view'] );
$err = new ErrorController(array(), $view);
//load the class and its method
//pass the params to it
$class = ucfirst( $controller . "Controller" );
if ( class_exists($class) )
{
$con = new $class($param, $view);
$act = $action . 'Action';
if ( method_exists($con, $act) )
{
$con->$act();
}
else
{
$view->setController("error");
$view->setAction('index');
$err->indexAction();
}
}
else
{
$view->setController("error");
$view->setAction('index');
$err->indexAction();
}
So this is my solution how i solved my problem. it is based on #AlexAtNet solution and parts of glue that was recommend to me by #Sean
Also you can try this way:
$controller = new $class();
$method = $action . 'Action';
[$controller, $method]()

Categories