I'm trying to learn MVC design pattern for web applications so I decided to write my own PHP MVC framework. Before writing this post I read a lot of tutorials and forums about MVC. Now I pretty well understanding the MVC idea, and how communicate controller-model-view. I have write router and few modules (login, categories, ...) - seems it's working.
Now I'm confused a bit:
If I call localhost/LogIn I get only login form, if I call localhost/categories I get category list. Everything OK, but I want to create index controller and when calling localhost/index I want see login form, categories and a lot more modules.
Should I call controllers (login, categories) from indexController.php?
I need advice how to concatenate needed modules in one page.
No, controllers shouldn't be calling each other's functions. Some frameworks introduce "helpers" to implement what you need.
Controllers can use the same models, and views anyway are going to be different, so you can use your Categories model to provide you categories to display (e.g. $categories->getCategoriesList()) and then using it in category controller view and also in index controller view.
A legitimate method of calling one controller from another is by forming an HTTP request - e.g. receiving an HTML snippet (another controller rendered view) to display in your view via AJAX or using an iframe with a source pointing to your another controller (which is a clumsy solution, mostly for idea illustration).
You need several things:
You need a .htaccess file that will cause all requests to go through your index file, here is a simple one:
RewriteEngine On
RewriteBase /demo
RewriteCond %{REQUEST_FILENAME} !-d
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-l
RewriteRule ^(.+)$ index.php?url=$1 [QSA,L]
In the index.php file you need to set the include path, so you won't have to explicitly include the modules/controllers/views or any other class you choose:
define("APPLICATION_PATH", realpath('.'));
$paths = array(
APPLICATION_PATH.'/controllers',
APPLICATION_PATH.'/models',
APPLICATION_PATH.'/views',
APPLICATION_PATH.'/libs',
APPLICATION_PATH.'/includes',
get_include_path()
);
set_include_path(implode(PATH_SEPARATOR, $paths));
now add the 'magic method' for autoloading classes (called automatically) and initialize your Bootsrap class:
function __autoload($className){
$fileName = str_replace('\\','/', $className);
require_once "$fileName.php";
}
new Bootstrap();
Bootstrap.php:
<?php
class Bootstrap {
public function __construct() {
$url = $_GET['url'];
$params = explode('/', $url);
//if controller exist - call it, else call login controller
if (isset($params[0]) && $params[0]){
$controller = new $params[0]();
}
else{
$controller = new login();
}
//if method exist - call it, else call index method
if (isset($params[1]) && $params[1]){
//if parameter exit - call method with param, else call witout param
if (isset($params[2]) && $params[2]){
$controller->$params[1]($params[2]);
}
else{
$controller->$params[1]();
}
}
else{
$controller->index();
}
}
}
That should give you a basic MVC Framework.
Use your controller (index.php) to centralized code that would be used on every page (request validators, error handles, exception handlers, session stuff).
Create a Router class to get the correct models. Allow the models to get the correct views. I have included some UML diagrams from my other answer (https://stackoverflow.com/questions/42172228/is-this-how-an-mvc-router-class-typically-works) to help out. Remember, try to program to an abstract interface, not to a concrete implementation.
Related
I am trying to figure out the best approach when linking to static pages using a loosely followed MVC design pattern.
I begin by rewriting all requests to the index.php which handles all request and break them down the url into the controller, action and parameters. However if i don't want to follow this url structure and just want to visit a static page such as 'http://example.com/home/' without having to call some action how would i achieve this without getting a php error caused by my router/dispatcher trying to request a file that does not exist?
I thought about setting up some switch statement or a if statement as shown below that checks if the url is set to something then uses a custom defined controller and action, or i wasn't sure whether to take the static resources out of the MVC directory and have it seperate and link to it that way?
<?php
class Router
{
static public function parse($url, $request)
{
$url = trim($url);
if ($url == "/")
{
$request->controller = "tasks";
$request->action = "index";
$request->params = [];
}
else
{
$explode_url = explode('/', $url);
$explode_url = array_slice($explode_url, 2);
$request->controller = $explode_url[0];
$request->action = $explode_url[1];
$request->params = array_slice($explode_url, 2);
}
}
}
?>
This works, but i'd rather not have a huge router setup for many different static resources as it feels tacky and that i am just patching together code. Would putting static pages in its own directory outside of MVC and linking to them in the views be a valid option? i'm relatively new to MVC so any guidance would be great.
Your application shouldn't receive request it is not supposed to handle, you can solve this on a webserver level:
if you are using apache for example, you can setup in the .htaccess file that the request should be directed to your front controller (ex: index.php) only if the requested resource does not exist
RewriteEngine on
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule ^.*$ /index.php [L]
I am trying to make website where users are able to post content using an WYSIWYG editor. In my .htaccess file i have
FallbackResource index.php
which is successfully sending all requests to the index.php file. I am then using PHP to require the desired file
$path = explode('/', $_SERVER['REQUEST_URI']);
//[SCRIPT_FILENAME] => C:/xampp/htdocs/tests/index.php
if($path[2] == 'notifications'){
require_once 'notifications.php';
}
All this works fine. The problem arises when i try to redirect to an external link like google.com. The link itself gets redirected to the index page instead of being redirected.
This is the key because users can insert tags for other websites as reference in their text with the WYSIWYG editors. So my question is, Am i doing this right or do i need a different approach? and if so, which approach?. I would greatly appreciate similar approaches to those used by sites like facebook or twitter.
AS for an approach I would use a method similar to CodeIgniter or other MVC frameworks ( but simpler ).
So first you have to devise a schema, what I mean by that is something like this
www.yoursite.com/index.php/{controller}/{method}/args ...
Then you would build a router to go parse the url. I can write you one but it will take a minute.
UPDATE
You can find it on my github page here
But for refrence here is the code:
SimpleRouter.php
/**
* A simple 1 level router
*
* URL schema is http://example.com/{controller}/{method}/{args ... }
*
* #author ArtisticPhoenix
* #package SimpleRouter
*/
class SimpleRouter{
/**
* should be the same as rewrite base in .htaccess
* #var string
*/
const REWRITE_BASE = '/MISC/Router/';
/**
* path to controller files
*
* #var string
*/
const CONTOLLER_PATH = __DIR__.'/Controllers/';
/**
* route a url to a controller
*/
public static function route(){
//normalize
if(self::REWRITE_BASE != '/'){
$uri = preg_replace('~^'.self::REWRITE_BASE.'~i', '',$_SERVER['REQUEST_URI']);
}
$uri = preg_replace('~^index\.php~i', '',$uri);
$uri = trim($uri,'/');
//empty url, like www.example.com
if(empty($uri)) $uri = 'home/index';
//empty method like www.example.com/home
if(!substr_count($uri, '/')) $uri .= '/index';
$arrPath = explode('/', $uri);
$contollerName = array_shift($arrPath);
$methodName = array_shift($arrPath);;
$contollerFile = self::CONTOLLER_PATH.$contollerName.'.php';
if(!file_exists($contollerFile)){
//send to error page
self::error404($uri);
return;
}
require_once $contollerFile;
if(!class_exists($contollerName)){
self::error404($uri);
return;
}
$Controller = new $contollerName();
if(!method_exists($Controller, $methodName)){
self::error404($uri);
return;
}
if(!count($arrPath)){
call_user_func([$Controller, $methodName]);
}else{
call_user_func_array([$Controller, $methodName], $arrPath);
}
}
/**
* call error 404
*
* #param string $uri
*/
protected static function error404($uri){
require_once self::CONTOLLER_PATH.'home.php';
$Controller = new home();
$Controller->error404($uri);
}
}
Default Controller home.php
/**
*
* The default controller
*
* #author ArtisticPhoenix
* #package SimpleRouter
*/
class home{
public function index($arg=false){
echo "<h3>".__METHOD__."</h3>";
echo "<pre>";
print_r(func_get_args());
}
public function otherpage($arg){
echo "<h3>".__METHOD__."</h3>";
echo "<pre>";
print_r(func_get_args());
}
public function error404($uri){
header('HTTP/1.0 404 Not Found');
echo "<h3>Error 404 page {$uri} not found</h3>";
}
}
2nd Controller (example) user.php
/**
*
* An example users router
*
* #author ArtisticPhoenix
* #package SimpleRouter
*/
class user{
public function index(){
echo "<h3>".__METHOD__."</h3>";
}
public function login(){
echo "<h3>".__METHOD__."</h3>";
}
}
Rewrite (remove index.php) .htaccess
RewriteEngine On
# For sub-foder installs set your RewriteBase including trailing and leading slashes
# your rewrite base will vary, possibly even being / if no sub-foder are involved
RewriteBase /MISC/Router/
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule ^(.*)$ index.php/$1 [L]
Finally index.php
require_once __DIR__.'/SimpleRouter.php';
SimpleRouter::route();
I went with a static call to the router just to keep things simple, this way no instance is needed of the class which only has 2 methods (1 public) anyway.
I built it as a single level router, what I mean by that is that you cannot put controllers in sub-folders, such as Controllers/user/home.php instead of just Controllers/user.php. This should be fine for small to medium sized sites, but for a vary large site it may be desirable to nest the controllers. However this adds a bunch of complexity to it.
The advantage of this way, instead of individually creating the routes should be obvious. You shouldn't have to touch the code as long as the route (the url) follows the simple schema I outlined. This also allows you organize the sections of your site in their own files by functionality. For example take the user.php controller. Would let you put all that functionality in one place, such as the login, profile, logout pages etc...
If you were to add a member.php controller you can put all the stuff that you only want to show to logged in users there. And then in the __construct method of that controller you check if the current session is logged in and it covers all methods for that file.
The url schema is this http://example.com/{controller}/{method}/{args ... }. But you can also use urls like this http://example.com/index.php/{controller}/{method}/{args ... }. I'm using Mod Rewrite to allow for the removal of the index.php in the URL, so if you don't have that then you have to put index.php in the URL ( or remove it by some other means )
REWRITE_BASE On my local test server I just place everything in it's own folder as I am to lazy to setup virtual hosts. So you would want to change this constant, and the matching .htaccess, value to be whatever your sub folder is ( maybe nothing ). I just left it here for example of how to use it within a sub-folder.
Lastly, this is the C in traditional MVC architecture. You should avoid doing what is called "Business logic" in the controllers, you should also avoid outputting HTML directly from the controller (although I did this in the examples). Instead of outputting the HTML, I would suggest using a template engine like Blade or Twig there are many of these freely available. Think of a Controller like the "Glue" that joins the "Model (business logic)" to the "View (template)". Business logic would be like a User class (uppercase U like a noun, not lowercase as in our user controller) that handles the Database functionality for users. So instead of putting that code right in the Controller you would build a "User Model" and import that into the controller. This is the "Right Way" to do it, but of course it adds some complexity up front, but you will find later on the organization of it to far outweigh that in time savings.
Now some examples
404 errors (and missed routes):
http://example.com/foo routes to home::error404() and outputs <h3>Error 404 page foo/index not found</h3>
http://example.com/home/foo routes to home::error404() and outputs <h3>Error 404 page home/foo not found</h3>
Default Home page:
http://example.com/ routes to home::index()
http://example.com/index.php routes to home::index()
http://example.com/home routes to home::index()
http://example.com/home/index routes to home::index()
http://example.com/home/index/hello routes to home::index('hello')
http://example.com/home/index/hello/world routes to home::index('hello','world')
As it stands you have to put the full path in for arguments for home, the above code could be changed to account for this but it has some repercussions for missing pages. Basically any missing page would become an argument of the controller. So if you had http://example.com/user/foo it would call user::index('foo') if that was done. Of course this too can be accounted for but the complexity starts to stack up.
http://example.com/home/otherpage routes to home::otherpage() but issues a warning for missing argument.
http://example.com/home/otherpage/hello routes to home::otherpage('hello')
http://example.com/index.php/home/otherpage/hello routes to home::otherpage('hello')
If you look: home::index($arg=false) defines a default, where this method does not. So this method requires an argument. The rest of this should be pretty self explanatory.
Second Controller user example:
http://example.com/user routes to user::index()
http://example.com/user/index routes to user::index()
http://example.com/user/login routes to user::login()
The rest of this is pretty much the same, I just wanted to show how to organize the controllers by functionally for the site.
I should note that this can handle an unlimited number of arguments following the {method} in the url.
Enjoy!
I asked a question like this before but since i still can't find an answer to this i'll ask it again :-s.
I'm using this very basic 'templating' script:
require_once 'core/init.php';
if(empty($_GET['page'])){
header('Location: home');
die();
}
$basePath = dirname(__FILE__) . '/';
$viewPath = $basePath . 'view/';
$view = scandir($viewPath);
foreach($view as $file)
{
if (!is_dir($viewPath . $file))
{
$pages[] = substr($file, 0, strpos($file, '.'));
}
}
if(in_array($_GET['page'], $pages)){
include($viewPath . $_GET['page'] . '.php');
} else{
include($basePath . '404.php');
}
and i'm rewriting my url from /base/index.php?page=somepage to /base/somepage(somepage is a .php file in my template folder) using this htaccess file
RewriteEngine On
RewriteBase /base/
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule ^([^?]*) index.php?page=$1 [L,QSA]
With 1 parameter it works just fine but my problem is that i don't know how to rewrite a second param /base/profile?user=username (with no htaccess file this would have look like this /base/index.php?page=profile?user=username) and i want it to look like this /base/profile/username.
I hope that this question is understandable :-s
Routing is a real issue and I can't be exhaustive in one comment, but I'll try to do my best. Please forgive my aproximative english and let me know if you don't understand. I still have a lot to learn so I'll try to explain through something I made, but it is probably fully improvable.
Today's PHP Standard Recommendation about routing and interpreting request should implement PSR7.
I personnaly use it through a FrontController Design Pattern in a MVC framework I'm building to understand these concepts. My folders are organized like this :
Public :
Where I launch my web server, where you can find JavaScript/CSS. There's an index.php which just contains
require_once('../index.php');
App :
Where there's the router and mostly all generic code
Src :
Where there's the specific code to the app. That means controllers and entities for now.
Vendor :
Composer dependencies such as GuzzleHTTP to have a class between the actual request and the code.
Here's the code in my root's index.php :
<?php
require_once 'vendor/autoload.php';
use Namespace\FrontController;
use \GuzzleHttp\Psr7\ServerRequest;
use function Http\Response\send;
$front_ctrl = new FrontController(ServerRequest::fromGlobals());
send($front_ctrl->getResponse());
The main point of it is that I interpret the request within an instance of a class implementing PSR7.
In my FrontController, my request travel through some methods (such as removing trailing slash) to finnaly be sent in a Router class to be handled.
The purpouse of my Router class is to check if the URI exist in the array where I stocked all my routes under this format in a json file :
{
"/": [
"AppController",
"indexAction",
["GET", "POST"]
],...
}
This is where I use regex to match variable inside the URI (/article/:id for example) too.
This Class can be resumed as "Does this URI exists in my app?".
At this point, I instantiate a new Route class with all the array as parameter. From here, I have to answer questions such as "Is it attached to a method in a controller ?", "Does the method in which it is asked is handled ?" ...
To summarize, at this point, I have an Instance of a Class that represents the Request, another one that represents all my routes. I confront them to get ONE Route which I'm gonna manipulate through an Instance of a Class Route.
If it passed all those tests, then I can instantiate the right Controller, where there will be the logical part specific of the app, requiring some action to get data, that I will send in my views to generate a HTML output which I will send back all the way back to my function send so the output is displayed when you ask for a specific URI.
The main point of this long answer is to show you something that is almost completely independent from the server. It's also useful if your app gets bigger and has to handle more specific rules for routing. It forces you to separates all the bundles of your app : A Controller is not a Model neither a Router...
Try to find some good tutorials to learn Oriented Object Programmation in PHP, which would avoid easy security issues and give you much more comfort when developping an app :)
Hope it was understandable and helpful
I have a problem building a front controller for a project I have at school.
Here is the thing, i created several controllers per entities i.e. One to select, another one to delete etc... and this for posts and comments. So i have 6 controllers and then I'm being asked to build front controller above these controllers but i keep on adding layers (route all to index.php then from there I instantiate the front controller which then will send the request to the related controller with header(location:...) and it gets messy and very complicated because for some of the request i need to pass some ids in the url for the db request...
I can't use any framework so is there a cleaner way to build a front controller that can handle request with parameters in the url and some without?
Do i have to route everything to a single page (with htaccess i created a simple rule to send everything to index.php)?
And then from there again instantiate the front controller?
If you need some code I'll share this with you but it's a mess :)
Thanks guys, i need advise I'm kind of lost at this point
You will want to build a basic router, what it does.
A router will parse the path and take parts of it and locate a controller file. And then run a method in that file with arguments. We will use this format for urls
www.yoursite.com/index.php/{controller}/{method}/{args}
So for this example our url will be
www.yoursite.com/index.php/home/index/hello/world
the index.php can be hidden using some basic .htaccess ( Mod Rewrite )
So lets define some things, first we will have folders
-public_html
index.php
--app
---controller
home.php
So the main folder is public_html with the index.php and a folder named app. Inside app is a folder named controller and inside that is our home.php contoller
Now for the Code ( yea )
index.php (basic router )
<?php
//GET URI segment from server, everything after index.php ( defaults to home )
$path_info = isset( $_SERVER['PATH_INFO'] ) ? $_SERVER['PATH_INFO'] : '/home';
//explode into an array - array_filter removes empty items such as this would cause '/home//index/' leading /, trailing / and // double slashes.
$args = array_filter( explode('/', $path_info) );
$controller_class = array_shift($args); //remove first item ( contoller )
$method = count( $args ) > 0 ? array_shift($args) : 'index'; //remove second item or default to index ( method )
$basepath = __DIR__.'/app/controller/'; //base path to controllers
if(!file_exists($basepath.$controller_class.".php") ){
echo "SHOW 404";
exit();
}
//instantiate controller class
require_once $basepath.$controller_class.".php";
$Controller = new $controller_class;
//check if method exists in controller
if(!method_exists( $Controller, $method ) ){
echo "Method not found in controller / or 404";
exit();
}
//call methods with any remaining args
call_user_func_array( [$Controller, $method], $args);
home.php ( controller )
<?php
class home{
public function index( $arg1="", $arg2=""){
echo "Arg1: ".$arg1 . "\n";
echo "Arg2: ".$arg2 . "\n";
}
public function test( $arg1 = "" ){
echo "Arg1: ".$arg1 . "\n";
}
}
Now if you put in any of these urls
www.yoursite.com/index.php
www.yoursite.com/index.php/home
www.yoursite.com/index.php/home/index
It should print ( defaults )
Arg1:
Arg2:
If you do this url
www.yoursite.com/index.php/home/index/hello/world
It should print
Arg1: hello
Arg2: world
And if you do this one
www.yoursite.com/index.php/home/test/hello_world
it would print
Arg1: hello_world
The last one, running in the second method test ( no echo arg2 ), this way you can see how we can add more controllers and methods with only having to code them into a controller.
This method still allows you to use the $_GET part of the url, as well as the URI part to pass info into the controller. So this is still valid
www.yoursite.com/index.php/home/test/hello_world?a=1
and you could ( in home::test() ) output the contents of $_GET with no issues, this is useful for search forms etc. Some pretty url methods prevent this which is just ... well ... crap.
In .htaccess with mod rewrite you would do this to remove index.php from the urls
<IfModule mod_rewrite.c>
RewriteEngine On
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule ^(.*)$ index.php?/$1 [L]
</IfModule>
Then you can use urls without the index.php such as this
www.yoursite.com/home/index/hello/world
This is the simplest Router I could come up with on such short notice, yes I just created it. But this is a very similar ( although simplified ) implementation used in many MVC frameworks
PS. please understand how this is all done, so you actually learn something...
Many improvements could be made, such as allowing these urls
www.yoursite.com/hello/world
www.yoursite.com/home/hello/world
www.yoursite.com/index/hello/world
Which would all fall back to the a default of home controller and index method, but that would take some additional checks (for not found files and methods ) I cant be bothered with right now...
Here's a simple demo process (won't work, just an example).
index.php
//Say you called /index.php/do/some/stuff
$route = $_SERVER["PATH_INFO"]; //This will contain /do/some/stuff
$router = new Router();
$router->handle($route);
router.php
class Router {
private static $routeTable = [
"do/some/stuff" => [ "DoingSomeController" => "stuff" ]
];
public function handle($route) {
if (isset(self::$routeTable[trim($route,"/"])) {
$controller = new self::$routeTable[trim($route,"/"][0];
call_user_func([$controller,self::$routeTable[trim($route,"/"][1]]);
}
}
}
Basic demo, what some frameworks do.
I'm not really new with Codeigniter but have been working on started projects so far. Now I'm starting a new small project on my own and I'm kinda lost.
I downloaded codeigniter, configured all the parameters in wamp so I have this base URL: http://local.project
Now I'm trying to build a small admin. I should be able to enter to this admin through http://local.project/admin which should show a login page. I already have a template for this.
The thing is that same 404 error appears. The configuration I have is this:
Inside config folder, routes.php:
$route['default_controller'] = "admin";
$route['404_override'] = '';
$route['admin'] = 'admin';
then on controllers folder, created another folder admin with a file also called admin.php which contains:
<?php
class Admin extends CI_Controller {
public function index()
{
echo 'Hello World!';
}
}
?>
now, trying to access from the browser I've tried many possible url but I'm still not sure of how it should be>
http://local.project/admin
and other combinations like
http://local.project/admin/index.php
http://local.project/index
http://local.project/index/admin
but always appears error 404 page not found.
So I'm really wondering, what am I doing wrong?
EDIT
this is what .htaccess contains>
RewriteEngine On
RewriteBase /
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule .* index.php/$0 [PT,L]
You're perfectly reasonable to assume that by defining a default controller it should work in subfolders, but ellislabs doesn't agree. As per the user guide:
$route['default_controller'] = 'welcome';
This route indicates which controller class should be loaded if the
URI contains no data, which will be the case when people load your
root URL. In the above example, the "welcome" class would be loaded.
You are encouraged to always have a default route otherwise a 404 page
will appear by default.
The key words there are "if the URI contains no data." "/admin" contains data. If you put your Admin controller in the root controllers folder and put in "local.project/" you'd get the index function of the Admin controller, but in a subfolder, you have to specify the full path, which in this case would be "/admin/admin/index." Putting that into your "admin" route will fix the issue, and then adding another route to take care of any other functions ($route["admin/(:any)'] = 'admin/admin/$1' should do the trick) will fix the rest.
And just so you know, if you ever upload this code to a server running Linux, it'll break because "Admin" and "admin" are two different controllers as far as Linux/Unix are concerned. As a long-time Windows dev, I feel your pain on that one.