I try to work with a simple Router class (learning basics before a framework, but I think I got something wrong with the example router I used. Below is a very small router class I got from a colleague and I tried to integrate it into my code to substitute previous uses where I just used echo before (commented out part of the code).
both loginController showLoggedInUser() and registerController index() are just used to render an html template.
Both $router->add() would work if I use it just to add a single route, however my router does not save multiple routes in the array because it seems every route will be saved under the key '/' and in case I provide mutiple routes it seems my previous routes are simply overwritten. So I guess I would need to adjust the Router class. How can I fix this?
PHP 7.4 used
Router.php
<?php
declare(strict_types=1);
class Router
{
private array $route;
public function add(string $url, callable $method): void
{
$this->route[$url] = $method;
}
public function run()
{
$path = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH);
if(!array_key_exists($path, $this->route))
{
exit();
}
return call_user_func($this->route[$path]);
}
}
index.php
<?php
declare(strict_types=1);
require __DIR__ . '/../vendor/autoload.php';
session_start();
$router = new Router();
$mysqliConnection = new MysqliConnection();
$session = new SessionService();
$loginController = new Login($mysqliConnection);
$router->add('/', [$loginController, 'showLoggedInUser']);
//echo $loginController->showLoggedInUser();
$registerController = new Register($mysqliConnection);
$router->add('/', [$registerController, 'index']);
//echo $registerController->index();
echo $router->run();
Not sure of the overall principle of having two routes with the same name, but you could achieve this using a list of callables for each route.
I've made some changes (including the callable passed for each route) to show the principle, but you should get the idea...
class Router
{
private array $route;
public function add(string $url, callable $method): void
{
$this->route[$url][] = $method;
}
public function run()
{
$path = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH);
if(!array_key_exists($path, $this->route))
{
exit();
}
foreach ( $this->route[$path] as $paths ) {
$paths();
}
// Not sure what to return in this case.
// return call_user_func($this->route[$path]);
}
}
$router = new Router();
// $mysqliConnection = new MysqliConnection();
// $session = new SessionService();
// $loginController = new Login($mysqliConnection);
$router->add('/', function () { echo "login"; } );
// $registerController = new Register($mysqliConnection);
$router->add('/', function () { echo "Register"; });
echo $router->run();
I would instead recommend having separate url's, /login and /register so that they can be called separately.
Related
I have successfuly installed Router using PHP-DI.
There is some simple app example based on the article.
There are 2 files: index.php and controller.php. I want to use $container from controller. However, I have no idea how to inject it?
// index.php
use...
require_once...
$containerBuilder = new ContainerBuilder();
....
$container = $containerBuilder->build(); // I succesfuilly build a container here with all needed definitions, including Database, Classes and so on.
$routes = simpleDispatcher(function (RouteCollector $r) {
$r->get('/hello', Controller::class);
});
$middlewareQueue[] = new FastRoute($routes);
$middlewareQueue[] = new RequestHandler($container);
$requestHandler = new Relay($middlewareQueue);
$response = $requestHandler->handle(ServerRequestFactory::fromGlobals());
$emitter = new SapiEmitter();
return $emitter->emit($response);
So the code just receive Response from the dispatcher and pass it off to the emitter.
namespace ExampleApp;
use Psr\Http\Message\ResponseInterface;
class Controller
{
private $foo;
private $response;
public function __construct(
string $foo,
ResponseInterface $response
) {
$this->foo = $foo;
$this->response = $response;
}
public function __invoke(): ResponseInterface
{
$response = $this->response->withHeader('Content-Type', 'text/html');
$response->getBody()
->write("<html><head></head><body>Hello, {$this->foo} world!</body></html>");
return $response;
}
}
Now I want to add logic into Controller based on my $container: database, logger and so on. I want somehow to use $container instance which was created in index.php. I have tried a lot of ways, but nothing works proper.
I've done quite a bit of reading on this and a lot of people are saying I should be using a singleton class. I was thinking of writing a "Config" class which would include a "config.php" file on __construct and loop through the $config array values and place them into $this->values...
But then I read more and more about how singletons should never be used. So my question is - what is the best approach for this? I want to have a configuration file, for organization purposes, that contains all of my $config variables. I do not want to be hardcoding things such as database usernames and passwords directly into methods, for the purpose of flexibility and whatnot.
For example, I use the following code in an MVC project I am working on:
public/index.php
<?php
include '../app/bootstrap.php';
?>
app/bootstrap.php
<?php
session_start();
function __autoload ($class) {
$file = '../app/'.str_replace('_', '/', strtolower($class)).'.php';
if (file_exists($file)) {
include $file;
}
else {
die($file.' not found');
}
}
$router = new lib_router();
?>
app/lib/router.php
<?php
class lib_router {
private $controller = 'controller_home'; // default controller
private $method = 'index'; // default method
private $params = array();
public function __construct () {
$this->getUrl()->route();
}
private function getUrl () {
if (isset($_GET['url'])) {
$url = trim($_GET['url'], '/');
$url = filter_var($url, FILTER_SANITIZE_URL);
$url = explode('/', $url);
$this->controller = isset($url[0]) ? 'controller_'.ucwords($url[0]) : $this->controller;
$this->method = isset($url[1]) ? $url[1] : $this->method;
unset($url[0], $url[1]);
$this->params = array_values($url);
}
return $this;
}
public function route () {
if (class_exists($this->controller)) {
if (method_exists($this->controller, $this->method)) {
call_user_func(array(new $this->controller, $this->method), $this->params);
}
else {
die('Method '.$this->method.' does not exist');
}
}
else {
die('Class '.$this->controller.' does not exist');
}
}
}
?>
Now let's say I visit http://localhost/myproject/lead/test
It is going to call the controller_lead class and the test method within.
Here is the code for app/controller/lead
<?php
class controller_lead extends controller_base {
public function test ($params) {
echo "lead test works!";
}
}
?>
app/controller/base
<?php
class controller_base {
private $db;
private $model;
public function __construct () {
$this->connect()->getModel();
}
private function connect () {
//$this->db = new PDO($config['db_type'] . ':host=' . $config['db_host'] . ';dbname=' . $config['db_name']. ', $config['db_user'], $config['db_pass'], $options);
return $this;
}
private function getModel () {
$model = str_replace('controller', 'model', get_class($this));
$this->model = new $model($this->db);
}
}
?>
This is where I run into the issue. As you can see, the connect method is going to try and create a new PDO object. Now how am I going to inject this $config variable, given all the other code I just provided?
My options appear to be:
Use a singleton (bad)
Use a global (worse)
Include config.php in bootstrap.php, and inject it throughout multiple classes (Why should I inject this into my lib_router class when lib_router has absolutely nothing to do with the database? This sounds like terrible practice.)
What other option do I have? I don't want to do any of those 3 things...
Any help would be greatly appreciated.
I ended up including a config file in my bootstrap which simply contained constants, and used the constants.
I ended up including a config file in my bootstrap which simply contained constants, and used the constants.
That was the same case for me - required the file in Model. The file based approach was the best fit since it has some benefits: you can .gitignore, set custom permission set, all your parameters are stored centrally, etc.
However, If the config file contains DB connection parameters only, I prefered to require the config file in Model only. Maybe, you could also break down into multiple, more specific config files and require them where necessary.
public function __construct()
{
if (self::$handle === FALSE)
{
$db = array();
require APP_DIR.DIR_SEP.'system'.DIR_SEP.'config'.DIR_SEP.'Database.php';
if (!empty($db))
{
$this->connect($db['db_host'], $db['db_user'], $db['db_password'], $db['db_name']);
}
else
{
Error::throw_error('Abimo Model : No database config found');
}
}
}
Originally, my Slim Framework app had the classic structure
(index.php)
<?php
$app = new \Slim\Slim();
$app->get('/hello/:name', function ($name) {
echo "Hello, $name";
});
$app->run();
But as I added more routes and groups of routes, I moved to a controller based approach:
index.php
<?php
$app = new \Slim\Slim();
$app->get('/hello/:name', 'HelloController::hello');
$app->run();
HelloController.php
<?php
class HelloController {
public static function hello($name) {
echo "Hello, $name";
}
}
This works, and it had been helpful to organize my app structure, while at the same time lets me build unit tests for each controler method.
However, I'm not sure this is the right way. I feel like I'm mocking Silex's mount method on a sui generis basis, and that can't be good. Using the $app context inside each Controller method requires me to use \Slim\Slim::getInstance(), which seems less efficient than just using $app like a closure can.
So... is there a solution allowing for both efficiency and order, or does efficiency come at the cost of route/closure nightmare?
I guess I can share what I did with you guys. I noticed that every route method in Slim\Slim at some point called the method mapRoute
(I changed the indentation of the official source code for clarity)
Slim.php
protected function mapRoute($args)
{
$pattern = array_shift($args);
$callable = array_pop($args);
$route = new \Slim\Route(
$pattern,
$callable,
$this->settings['routes.case_sensitive']
);
$this->router->map($route);
if (count($args) > 0) {
$route->setMiddleware($args);
}
return $route;
}
In turn, the Slim\Route constructor called setCallable
Route.php
public function setCallable($callable)
{
$matches = [];
$app = $this->app;
if (
is_string($callable) &&
preg_match(
'!^([^\:]+)\:([a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*)$!',
$callable,
$matches
)
) {
$class = $matches[1];
$method = $matches[2];
$callable = function () use ($class, $method) {
static $obj = null;
if ($obj === null) {
$obj = new $class;
}
return call_user_func_array([$obj, $method], func_get_args());
};
}
if (!is_callable($callable)) {
throw new \InvalidArgumentException('Route callable must be callable');
}
$this->callable = $callable;
}
Which is basically
If $callable is a string and (mind the single colon) has the format ClassName:method then it's non static, so Slim will instantiate the class and then call the method on it.
If it's not callable, then throw an exception (reasonable enough)
Otherwise, whatever it is (ClassName::staticMethod, closure, function name) it will be used as-is.
ClassName should be the FQCN, so it's more like \MyProject\Controllers\ClassName.
The point where the controller (or whatever) is instantiated was a good opportunity to inject the App instance. So, for starters, I overrode mapRoute to inject the app instance to it:
\Util\MySlim
protected function mapRoute($args)
{
$pattern = array_shift($args);
$callable = array_pop($args);
$route = new \Util\MyRoute(
$this, // <-- now my routes have a reference to the App
$pattern,
$callable,
$this->settings['routes.case_sensitive']
);
$this->router->map($route);
if (count($args) > 0) {
$route->setMiddleware($args);
}
return $route;
}
So basically \Util\MyRoute is \Slim\Route with an extra parameter in its constructor that I store as $this->app
At this point, getCallable can inject the app into every controller that needs to be instantiated
\Util\MyRoute.php
public function setCallable($callable)
{
$matches = [];
$app = $this->app;
if (
is_string($callable) &&
preg_match(
'!^([^\:]+)\:([a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*)$!',
$callable,
$matches
)
) {
$class = $matches[1];
$method = $matches[2];
$callable = function () use ($app, $class, $method) {
static $obj = null;
if ($obj === null) {
$obj = new $class($app); // <--- now they have the App too!!
}
return call_user_func_array([$obj, $method], func_get_args());
};
}
if (!is_callable($callable)) {
throw new \InvalidArgumentException('Route callable must be callable');
}
$this->callable = $callable;
}
So there it is. Using this two classes I can have $app injected into whatever Controller I declare on the route, as long as I use a single colon to separate controller from method. Using paamayim nekudotayim will call the method as static and therefore will throw an error if I try to access $this->app inside it.
I ran tests using blackfire.io and... the performance gain is negligible.
Pros:
this saves me the pain of calling $app = \Slim\Slim::getInstance() on every static method call accounting for about 100 lines of text overall.
it opens the way for further optimization by making every controller inherit from an abstract controller class, which in turn wraps the app methods into convenience methods.
it made me understand Slim's request and response lifecycle a little better.
Cons:
performance gains are negligible
you have to convert all your routes to use a single colon instead of paamayin, and all your controller methods from static to dynamic.
inheritance from Slim base classes might break when they roll out v 3.0.0
Epilogue: (4 years later)
In Slim v3 they removed the static accessor. In turn, the controllers are instantiated with the app's container, if you use the same convention FQCN\ClassName:method. Also, the method receives the request, response and $args from the route. Such DI, much IoC. I like it a lot.
Looking back on my approach for Slim 2, it broke the most basic principle of drop in replacement (Liskov Substitution).
class Route extends \Slim\Route
{
protected $app;
public function __construct($app, $pattern, $callable, $caseSensitive = true) {
...
}
}
It should have been
class Route extends \Slim\Route
{
protected $app;
public function __construct($pattern, $callable, $caseSensitive = true, $app = null) {
...
}
}
So it wouldn't break the contract and could be used transparently.
I'm trying to build a CMS using the MVC pattern. The structure of my CMS is as follows:
->index.php: entry point. Contains the appropriate includes and creates the frontController which takes a router as it's parameter:
$frontController = new FrontController(new Router);
echo $frontController->output();
->router.php: analyses the URL and defines what the names of each Model,View and Controller will be. By default they are preceded by home, but if the url is of the form http://localhost/index.php?route=Register the MVC classes will be named RegisterModel, RegisterView and Register Controller.
if(isset ( $_GET ['route'] ))
{
$this->URL = explode ( "/", $_GET ['route'] );
$this->route = $_GET ['route'];
$this->model = ucfirst($this->URL [0] . "Model");
$this->view = ucfirst($this->URL [0] . "View");
$this->controller = ucfirst($this->URL [0] . "Controller");
}
else
{
$this->model = "HomeModel";
$this->view = "HomeView";
$this->controller = "HomeController";
$this->route = "Home";
}
->frontController.php: This is where I am stuck. When I go to the homepage, it can be visualised correctly because I already have the default HomeModel,HomeView and HomeController classes created. But I created a link that points to register (localhost/index.php?route=Register) but the PHP log indicates that the appropriate Register classes weren't created by the frontController class.
class FrontController
{
private $controller;
private $view;
public function __construct(Router $router)
{
$modelName = $router->model;
$controllerName = $router->controller;
$viewName = $router->view;
$model = new $modelName ();
$this->controller = new $controllerName ( $model );
$this->view = new $viewName ( $router->getRoute(), $model );
if (! empty ( $_GET['action'] ))
$this->controller->{$_GET['action']} ();
}
public function output()
{
// This allows for some consistent layout generation code
return $this->view->output ();
}
}
At this moment I have no idea how to go about solving this issue. And even if I get the classes to be created in the frontController, is there a way to specify that the classes being dynamically generated should extend from a base Model,View,Controller class?
The default HomeView.php looks like this:
class HomeView
{
private $model;
private $route;
private $view_file;
public function __construct($route, HomeModel $model)
{
$this->view_file = "View/" . $route . "/template.php";
echo $this->view_file;
$this->route = $route;
$this->model = $model;
}
public function output()
{
require($this->view_file);
}
}
Any indications on anything that might help me get unstuck or a pointer in the right direction would be much appreciated.
EDIT1:
I forgot to add a summary of my two issues:
1. I would like to understand why the classes aren't being created in the FrontController class...
2. Once created how would I access those classes? Answer is in the comment section. Using the PHP spl_autoload_register function.
Thanks All!
As you know, Zend Framework (v1.10) uses routing based on slash separated params, ex.
[server]/controllerName/actionName/param1/value1/param2/value2/
Queston is: How to force Zend Framework, to retrive action and controller name using standard PHP query string, in this case:
[server]?controller=controllerName&action=actionName¶m1=value1¶m2=value2
I've tried:
protected function _initRequest()
{
// Ensure the front controller is initialized
$this->bootstrap('FrontController');
// Retrieve the front controller from the bootstrap registry
$front = $this->getResource('FrontController');
$request = new Zend_Controller_Request_Http();
$request->setControllerName($_GET['controller']);
$request->setActionName($_GET['action']);
$front->setRequest($request);
// Ensure the request is stored in the bootstrap registry
return $request;
}
But it doesn't worked for me.
$front->setRequest($request);
The line only sets the Request object instance. The frontController still runs the request through a router where it gets assigned what controller / action to call.
You need to create your own router:
class My_Router implements Zend_Controller_Router_Interface
{
public function route(Zend_Controller_Request_Abstract $request)
{
$controller = 'index';
if(isset($_GET['controller'])) {
$controller = $_GET['controller'];
}
$request->setControllerName($controller);
$action = 'index';
if(isset($_GET['action'])) {
$action = $_GET['action'];
}
$request->setActionName($action);
}
}}
Then in your bootstrap:
protected function _initRouter()
{
$this->bootstrap('frontController');
$frontController = $this->getResource('frontController');
$frontController->setRouter(new My_Router());
}
Have you tried: $router->removeDefaultRoutes(), then $request->getParams() or $request->getServer()?