I have a problem with PHP 8 routing. I'm creating my MVC "framework" to learn more about OOP. I did routing, controller and view - it works, I'm happy. But I found out that I would test my creation. This is where the problem begins, namely if the path is empty (""), it returns the "Home" view as asked in the routing, but if I enter "/ test" in the URL, for example, I have a return message "404 Not Found - The requested URL was not found on this server. " even though the given route is added to the table. What is wrong?
.htaccess file
RewriteEngine on
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule ^(.+)$ index.php [QSA,L]
index.php with routes
<?php
/*
* Require Composer
*/
require dirname(__DIR__) . '/vendor/autoload.php';
/*
* Routing
*/
$routes = new Core\Router();
$routes->new('', ['controller' => 'HomeController', 'method' => 'index']);
$routes->new('/test', ['controller' => 'HomeController', 'method' => 'test']);
$routes->redirectToController($_SERVER['QUERY_STRING']);
Router.php file
<?php
namespace Core;
class Router
{
/*
* Routes and parameters array
*/
protected $routes = [];
protected $params = [];
/*
* Add a route to the route board
*/
public function new($route, $params = [])
{
/*
* Converting routes strings to regular expressions
*/
$route = preg_replace('/\//', '\\/', $route);
$route = preg_replace('/\{([a-z]+)\}/', '(?P<\1>[a-z-]+)', $route);
$route = '/^' . $route . '$/i';
$this->routes[$route] = $params;
}
/*
* Get all routes from table
*/
public function getRoutesTable() {
return $this->routes;
}
/*
* Checking if the specified path exists in the route table
* and completing the parameters table
*/
public function match($url)
{
foreach($this->routes as $route => $params) {
if(preg_match($route, $url, $matches)) {
foreach($matches as $key => $match) {
if(is_string($key)) {
$params[$key] = $match;
}
}
$this->params = $params;
return true;
}
}
return false;
}
/*
* Get all params from table
*/
public function getParamsTable() {
return $this->params;
}
/*
* Redirection to the appropriate controller action
*/
public function redirectToController($requestUrl) {
$requestUrl = explode('?', $requestUrl);
$url = $requestUrl[0];
if ($this->match($url)) {
$controller = $this->params['controller'];
$controller = "App\\Controllers\\$controller";
if(class_exists($controller)) {
$controller_obj = new $controller($this->params);
$method = $this->params['method'];
if(method_exists($controller, $method)) {
$controller_obj->$method();
} else {
echo "Method $method() in controller $controller does not exists!";
}
} else {
echo "Controller $controller not found!";
}
} else {
echo "No routes matched!";
}
}
}
View.php file
<?php
namespace Core;
class View {
public static function view($file) {
$filename = "../App/Views/$file.php";
if(file_exists($filename)) {
require_once $filename;
} else {
echo "View $file is not exist!";
}
}
}
HomeController file
<?php
namespace App\Controllers;
use Core\Controller;
use Core\View;
class HomeController extends Controller {
public function index() {
return View::view('Home');
}
public function test() {
return View::view('Test');
}
}
Folder structure
Routes in web browser
I'm using XAMPP with PHP 8.0 and Windows 10.
First of all, choose one of the following two options to proceed.
Option 1:
If the document root is set to be the same as the project root, e.g. path/to/htdocs, in the config file of the web server (probably httpd.conf), similar to this:
DocumentRoot "/path/to/htdocs"
<Directory "/path/to/htdocs">
AllowOverride All
Require all granted
</Directory>
then create the following .htaccess in the project root, remove any .htaccess file from the directory path/to/htdocs/Public and restart the web server:
<IfModule dir_module>
DirectoryIndex /Public/index.php /Public/index.html
</IfModule>
Options FollowSymLinks
RewriteEngine On
RewriteBase /Public/
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule ^(.*)$ index.php [QSA,L]
Option 2:
If the document root is set to be the directory path/to/htdocs/Public in the config file of the web server (probably httpd.conf), similar to this:
DocumentRoot "/path/to/htdocs/Public"
<Directory "/path/to/htdocs/Public">
AllowOverride All
Require all granted
</Directory>
then create the following .htaccess in the directory path/to/htdocs/Public, remove any other .htaccess file from the project root (unless it, maybe, contains some relevant settings, other than DirectoryIndex) and restart the web server::
Options FollowSymLinks
RewriteEngine On
RewriteBase /
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule ^(.*)$ index.php [QSA,L]
After you decided and proceeded with one of the options above, edit the page index.php as shown bellow:
<?php
//...
/*
* NOTE: A route pattern can not be an empty string.
* If only the host is given as URL address, the slash
* character (e.g. "/") will be set as value of the
* URI path, e.g. of $_SERVER['REQUEST_URI']. Therefore,
* the route pattern should be set to "/" as well.
*/
$routes->new('/', [
'controller' => 'HomeController',
'method' => 'index',
]);
//...
/*
* In order to get the URI path, you must use the server
* parameter "REQUEST_URI" instead of "QUERY_STRING".
*/
$routes->redirectToController(
$_SERVER['REQUEST_URI']
);
Notes:
In regard of changing $_SERVER['QUERY_STRING'] to $_SERVER['REQUEST_URI'], the credits go to #MagnusEriksson, of course.
There can't be an "MVC framework", but a "web framework for MVC-based application(s)". This is, because a framework is just a collection of libraries (not at all correlated with MVC), which, in turn, is used by one or more applications implementing the MVC pattern.
For any further questions, don't hesitate to ask us.
Related
I know that you can add rules in htaccess, but I see that PHP frameworks don't do that and somehow you still have pretty URLs. How do they do that if the server is not aware of the URL rules?
I've been looking Yii's url manager class but I don't understand how it does it.
This is usually done by routing all requests to a single entry point (a file that executes different code based on the request) with a rule like:
# Redirect everything that doesn't match a directory or file to index.php
RewriteCond %{REQUEST_FILENAME} !-d
RewriteCond %{REQUEST_FILENAME} !-f
RewriteRule .* index.php [L]
This file then compares the request ($_SERVER["REQUEST_URI"]) against a list of routes - a mapping of a pattern matching the request to a controller action (in MVC applications) or another path of execution. Frameworks often include a route that can infer the controller and action from the request itself, as a backup route.
A small, simplified example:
<?php
// Define a couple of simple actions
class Home {
public function GET() { return 'Homepage'; }
}
class About {
public function GET() { return 'About page'; }
}
// Mapping of request pattern (URL) to action classes (above)
$routes = array(
'/' => 'Home',
'/about' => 'About'
);
// Match the request to a route (find the first matching URL in routes)
$request = '/' . trim($_SERVER['REQUEST_URI'], '/');
$route = null;
foreach ($routes as $pattern => $class) {
if ($pattern == $request) {
$route = $class;
break;
}
}
// If no route matched, or class for route not found (404)
if (is_null($route) || !class_exists($route)) {
header('HTTP/1.1 404 Not Found');
echo 'Page not found';
exit(1);
}
// If method not found in action class, send a 405 (e.g. Home::POST())
if (!method_exists($route, $_SERVER["REQUEST_METHOD"])) {
header('HTTP/1.1 405 Method not allowed');
echo 'Method not allowed';
exit(1);
}
// Otherwise, return the result of the action
$action = new $route;
$result = call_user_func(array($action, $_SERVER["REQUEST_METHOD"]));
echo $result;
Combined with the first configuration, this is a simple script that will allow you to use URLs like domain.com/about. Hope this helps you make sense of what's going on here.
I'm currently testing my project which has MVC integrated in it upon deploying in a free hosting site i've encountered an error 403 this error didn't appear since the development stage. My current knowledge why this error occurs is maybe the type of directives i've used in my htaccess? or something maybe in the core itself any toughts or suggestions to this particular problem :)
domain/
<IfModule mod_rewrite.c>
RewriteEngine on
RewriteRule ^$ public/ [L]
RewriteRule (.*) public/$1 [L]
</IfModule>
domain/public
<IfModule mod_rewrite.c>
Options -Multiviews
RewriteEngine On
RewriteBase /domain/public
RewriteCond %{REQUEST_FILENAME} !-d
RewriteCond %{REQUEST_FILENAME} !-f
RewriteRule ^(.+)$ index.php?url=$1 [QSA,L]
</IfModule>
domain/app
Options -Indexes
domain/app/libraries/Core.php
<?php
class Core {
protected $currentController = 'Pages';
protected $currentMethod = 'index';
protected $params = [];
public function __construct(){
//print_r($this->getUrl());
$url = $this->getUrl();
// Look in BLL for first value\
if($url != NULL){
if(file_exists('../app/controllers/' . ucwords($url[0]). '.php')){
// If exists, set as controller
$this->currentController = ucwords($url[0]);
// Unset 0 Index
unset($url[0]);
}
}
// Require the controller
require_once '../app/controllers/'. $this->currentController . '.php';
// Instantiate controller class
$this->currentController = new $this->currentController;
// Check for second part of url
if(isset($url[1])){
// Check to see if method exists in controller
if(method_exists($this->currentController, $url[1])){
$this->currentMethod = $url[1];
// Unset 1 index
unset($url[1]);
}
}
// Get params
$this->params = $url ? array_values($url) : [];
// Call a callback with array of params
call_user_func_array([$this->currentController, $this->currentMethod], $this->params);
}
public function getUrl(){
if(isset($_GET['url'])){
$url = rtrim($_GET['url'], '/');
$url = filter_var($url, FILTER_SANITIZE_URL);
$url = explode('/', $url);
return $url;
}
}
}
I am working on creating my own framework in php.
Everything is going as planned except working the framework according to the URL.
I cannot understand this:
Normal URL loading from www.mydomain.com/folderone/foldertwo/index.php
Zend Framework URL loading from the same URL would be
www.mydomain.com/folderone(controller)/folder2(action)/variables
how can i create that logic?
What am i missing?
I am really dedicated to create this framework.
I had the same task as I setup my framework. This is a solution work for me.
Create first your .htaccess file. Set up your rewrite conditions and exclude your template path. You can do it like that (just copy & paste):
RewriteEngine On
Options +Indexes
Options +FollowSymLinks
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteCond %{DOCUMENT_ROOT}/index\.php -f
RewriteCond %{DOCUMENT_ROOT}/template -d
RewriteRule ^(.*)$ index\.php?$1 [QSA]
Before I go ahead, we have to create five directories at least:
/var/www/models/
/var/www/controllers/
/var/www/classes/
/var/www/system/
/var/www/template/
Now, I added an auto loading in my index.php:
<?php
error_reporting(E_ALL ^ E_NOTICE);
session_start();
function autoload($class_name)
{
$autoloadDirs = array('models', 'classes', 'controllers');
foreach($autoloadDirs as $dir)
{
if(file_exists($dir.'/'.$class_name.'.php'))
{
require_once($dir.'/'.$class_name.'.php');
}
}
}
spl_autoload_register('autoload');
require_once('system/Calling.php');
Calling::Run();
?>
At the script above you'll see that require_once within Calling.php
class Calling
{
public static function Run($querystring = null)
{
//1. Parameter = Contollername
//2. Parameter = Action
$qString = preg_replace('/(\/$|^\/)/','',$querystring === null ? $_SERVER['QUERY_STRING'] : $querystring);
$callParam = !empty($qString) ? explode('/', $qString) : array();
$controllerName = count($callParam) > 0 ? (class_exists(ucfirst($callParam[0]).'Controller') ? ucfirst(array_shift($callParam)) : 'Error') : 'Main';
//All controllers have suffix "Controller" -> NameController.php
//and class name ike NameController
//If no controller name given, use MainController.php
$controllerClassName = $controllerName.'Controller';
//All public methods have suffix "Action" -> myMethodAction
//If there is no method named, use DefaultAction
$actionName = count($callParam) > 0 && method_exists($controllerClassName, ucfirst($callParam[0]).'Action') ? ucfirst(array_shift($callParam)) : 'Default';
$actionFunctionName = $actionName.'Action';
//Fetch the params
$param = new stdClass();
for($i = 0; $i < count($callParam); $i += 2)
{
$param->{$callParam[$i]} = isset($callParam[$i + 1]) ? $callParam[$i+1] : null;
}
////////////////////////////////////////////////////////////
//Init the Controller
$controller = new $controllerClassName($controllerName, $actionName);
$controller->$actionFunctionName($param);
////////////////////////////////////////////////////////////
//If you adapt this code: Is up to you to extends your controller
//from an internal controller which has the method Display();
$controller->Display();
}
}
Further, in your controller directory add your first controller namend MainController.php
//--> just better if you have also an internal controller with your global stuff
//--> class MainController extends Controller
class MainController
{
/** This is the default action
* #param $params
* #access public
* #return
*/
public function DefaultAction(stdClass $params)
{
//-> Do your staff here
}
/** This is the second action
* #param $params
* #access public
* #return
*/
public function SecondAction(stdClass $params)
{
//-> Do your staff here
}
/** This is the view handling method which has to run at least
* and I recommend to set up an internal controller and to extend
* all other controller from it and include this method in your
* internal controller
* #param
* #access
* #return
*/
public function Display()
{
//-> Run your template here
}
?>
Now, you can call the controller, methods and params like that:
//-> Load the main controller with default action
www.example.com/
//-> Load the main controller with default action
www.example.com/main/default/
//-> Load the main controller with second action
www.example.com/main/second/
//-> Load the main controller with second action and gives two params
www.example.com/main/second/key1/value1/key2/value2/
And now you'll have the following files and directories to start up your own framework.
/var/www/.htaccess
/var/www/index.php
/var/www/controllers/MainController.php
/var/www/system/Calling.php
/var/www/models/
/var/www/classes/
/var/www/template/
Enjoy your new basic framework kit
Zend FrameWork and most MVC frameworks use a BootStrap.
It routes the URL (using .htaccess) to one file let's say (index.php) by using something like that:
RewriteEngine on
RewriteRule !\.(js|ico|gif|jpg|png|css|html)$ index.php
Which loads the bootstrap and the routing class that takes whatever data it needs from the URL.
According to ZEND manual:
Routing is the process of taking a URI endpoint (that part of the URI
which comes after the base URL) and decomposing it into parameters to
determine which module, controller, and action of that controller
should receive the request. This values of the module, controller,
action and other parameters.
Routing occurs only once: when
the request is initially received and before the first controller is
dispatched.
EDIT: Answer for your comment:
Far away from ZEND FRAMEWORK, that's a code to test in a new PHP file:
<?php
$url = 'http://test.com/test1/test2/news.php';
$parse = parse_url($url);
$tokens = explode("/", $parse[path]);
print_r($tokens);
?>
If you want to build a router, you could look at Glue PHP for giving you examples.
It's a micro-framework that just do the routing part.
I know that you can add rules in htaccess, but I see that PHP frameworks don't do that and somehow you still have pretty URLs. How do they do that if the server is not aware of the URL rules?
I've been looking Yii's url manager class but I don't understand how it does it.
This is usually done by routing all requests to a single entry point (a file that executes different code based on the request) with a rule like:
# Redirect everything that doesn't match a directory or file to index.php
RewriteCond %{REQUEST_FILENAME} !-d
RewriteCond %{REQUEST_FILENAME} !-f
RewriteRule .* index.php [L]
This file then compares the request ($_SERVER["REQUEST_URI"]) against a list of routes - a mapping of a pattern matching the request to a controller action (in MVC applications) or another path of execution. Frameworks often include a route that can infer the controller and action from the request itself, as a backup route.
A small, simplified example:
<?php
// Define a couple of simple actions
class Home {
public function GET() { return 'Homepage'; }
}
class About {
public function GET() { return 'About page'; }
}
// Mapping of request pattern (URL) to action classes (above)
$routes = array(
'/' => 'Home',
'/about' => 'About'
);
// Match the request to a route (find the first matching URL in routes)
$request = '/' . trim($_SERVER['REQUEST_URI'], '/');
$route = null;
foreach ($routes as $pattern => $class) {
if ($pattern == $request) {
$route = $class;
break;
}
}
// If no route matched, or class for route not found (404)
if (is_null($route) || !class_exists($route)) {
header('HTTP/1.1 404 Not Found');
echo 'Page not found';
exit(1);
}
// If method not found in action class, send a 405 (e.g. Home::POST())
if (!method_exists($route, $_SERVER["REQUEST_METHOD"])) {
header('HTTP/1.1 405 Method not allowed');
echo 'Method not allowed';
exit(1);
}
// Otherwise, return the result of the action
$action = new $route;
$result = call_user_func(array($action, $_SERVER["REQUEST_METHOD"]));
echo $result;
Combined with the first configuration, this is a simple script that will allow you to use URLs like domain.com/about. Hope this helps you make sense of what's going on here.
I read this post and I want to use similar solution, but with db.
In my site controller after():
$theme = $page->get_theme_name(); //Orange
Kohana::set_module_path('themes', Kohana::get_module_path('themes').'/'.$theme);
$this->template = View::factory('layout')
I checked with firebug:
fire::log(Kohana::get_module_path('themes')); // D:\tools\xampp\htdocs\kohana\themes/Orange
I am sure that path exists. I have directly in 'Orange' folder 'views' folder with layout.php file.
But I am getting: The requested view layout could not be found
Extended Kohana_Core is just:
public static function get_module_path($module_key) {
return self::$_modules[$module_key];
}
public static function set_module_path($module_key, $path) {
self::$_modules[$module_key] = $path;
}
Could anybody help me with solving that issue?
Maybe it is a .htaccess problem:
# Turn on URL rewriting
RewriteEngine On
# Put your installation directory here:
# If your URL is www.example.com/kohana/, use /kohana/
# If your URL is www.example.com/, use /
RewriteBase /kohana/
# Protect application and system files from being viewed
RewriteCond $1 ^(application|system|modules)
# Rewrite to index.php/access_denied/URL
RewriteRule ^(.*)$ / [PT,L]
RewriteRule ^(media) - [PT,L]
RewriteRule ^(themes) - [PT,L]
# Allow these directories and files to be displayed directly:
# - index.php (DO NOT FORGET THIS!)
# - robots.txt
# - favicon.ico
# - Any file inside of the images/, js/, or css/ directories
RewriteCond $1 ^(index\.php|robots\.txt|favicon\.ico|static)
# No rewriting
RewriteRule ^(.*)$ - [PT,L]
# Rewrite all other URLs to index.php/URL
RewriteRule ^(.*)$ index.php/$1 [PT,L]
Could somebody help?
What I am doing wrong?
Regards
[EDIT]
My controller code:
class Controller_Site extends Controller_Fly {
public static $meta_names = array('keywords', 'descriptions', 'author');
public function action_main() {
$this->m('page')->get_main_page();
}
public function action_page($page_title) {
$this->m('page')->get_by_link($page_title);
}
public function after() {
$page = $this->m('page');
$metas = '';
foreach(self::$meta_names as $meta) {
if (! empty($page->$meta)) {
$metas .= html::meta($page->$meta, $meta).PHP_EOL;
}
}
$theme = $page->get_theme_name();
Kohana::set_module_path('themes', Kohana::get_module_path('themes').'/'.$theme);
$menus = $page->get_menus();
$this->template = View::factory('layout')
->set('theme', $theme)
->set('metas', $metas)
->set('menus', $menus['content'])
->set('sections', $page->get_sections())
->set_global('page', $page);
if ($page->header_on) {
$settings = $this->m('setting');
$this->template->header = View::factory('/header')
->set('title', $settings->title)
->set('subtitle', $settings->subtitle)
->set('menus', $menus['header']);
}
if ($page->sidebar_on) {
$this->template->sidebar = View::factory('sidebar', array('menus' => $menus['sidebar']));
}
if ($page->footer_on) {
$this->template->footer = View::factory('footer');
}
parent::after();
}
}
and parent controller:
abstract class Controller_Fly extends Controller_Template {
protected function m($model_name, $id = NULL) {
if (! isset($this->$model_name)) {
$this->$model_name = ORM::factory($model_name, $id);
}
return $this->$model_name;
}
protected function mf($model_name, $id = NULL) {
return ORM::factory($model_name, $id);
}
}
[Edit 2]
Previous link to post was dead, link was:
http://forum.kohanaframework.org/comments.php?DiscussionID=5744&page=1#Item_0
My guess is that your controller has a __construct() method and you aren't calling parent::__construct
Actually, I think for kohana V3 __construct needs to also be passed the $request object as follows:
public function __construct(Request $request)
{
parent::__construct($request);
}
I realized that I need to init all modules once again:
$theme = $page->get_theme_name();
Kohana::set_module_path('themes', Kohana::get_module_path('themes').'/'.$theme);
Kohana::modules(Kohana::get_modules());
Now I don't have an error. Instead I am getting white screen of death:
$this->template is null
:(
[EDIT]
Now I know, I had:
$this->template = View::factory('layout')
->set('theme', $theme)
->set('metas', $metas)
->set('menus', $menus['content'])
->set('sections', $page->get_sections())
->set_gobal('page', $page);
Ofcourse I forgot that set_global doesn't return $this :D
Anyway everything working now :)
I have one more question about efficiency. I am calling Kohana::modules() second time in request flow. Is this a big deal according to efficiency?
Regards