Just about a year ago I started creating web applications using the MVC design pattern. I started my journey with these series which provided me with some handles to create my own MVC application. Very basic, but straightforward.
Then I stumbled upon the S.O.L.I.D. principles defined by Robert C. Martin. Just by learning each principle apart I get how I could write maintainable code for the future. That's when I found out I have been breaking all 5 principles on a daily basis, but just because my code works I didn't try to respect them at all. So why not combine MVC with SOLID? This presentation by Stefan Priebsch gave me some insight on how to mix them together. Stefan proposes to create a single Controller for every single action that needs to be taken. That makes sense, it would result in clean, easy to read and maintainable code.
Now onto my problem. I've got an .htaccess file that does the URL rewriting. For example: http://website.com/user/create would create a user and http://website.com/user/email would send a new user an e-mail with account information. Both URLs go to the 'user' controller and execute methods 'create' and 'email'. Now this violates the Single Responsibility Principle. The 'user' controller would both have a function to create AND to email a user. It would be best to change the code and create two controllers:
class CreateUserController extends Controller {
public function create() {
}
}
class EmailUserController extends Controller {
public function email() {
}
}
This would mean that my URLs would be http://website.com/CreateUserController and http://website.com/EmailUserController. But that is not what I want right? /user/create would lead to CreateUserController and /user/email would lead to EmailUserController. How would I do something like this?
My router
This is the router I use. $url[0] is the controller and $url[1] the method within that controller.
class App {
protected $controller = 'home';
protected $method = 'index';
protected $params = [];
public function __construct() {
$url = $this->parseUrl();
if(file_exists('../app/controllers/' . $url[0] . '.php')) {
$this->controller = $url[0];
unset($url[0]);
}
require_once '../app/controllers/' . $this->controller . '.php';
$this->controller = new $this->controller;
if(isset($url[1])) {
if(method_exists($this->controller, $url[1])) {
$this->method = $url[1];
unset($url[1]);
} else {
header('location: /home/error');
}
}
$this->params = $url ? array_values($url) : [];
call_user_func_array([$this->controller, $this->method], $this->params);
}
public function parseUrl() {
if(isset($_GET['url'])) {
return $url = explode('/', filter_var(rtrim($_GET['url'], '/'), FILTER_SANITIZE_URL));
}
}
}
My base Controller
class Controller {
protected function model($model, $data = []) {
require_once APP . '/models/' . $model . '.php';
return new $model($data);
}
protected function view($view, $data = []) {
require_once APP . '/views/' . $view . '.php';
}
}
Related
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.
I am currently learning MVC pattern and I have a problem with understanding how to pass data from controller to model and then from model to view.
My structure looks like this
/controllers
index.php
/libs
controller.php
main.php
view.php
/models
index_model.php
/views
index.php
.htaccess
index.php
First I always create an main object and include files that i need
include 'libs/Main.php';
require 'libs/Controller.php';
require 'libs/view.php';
$main = new Main();
then in main class i reroute the url so correct controller is called
class Main {
public function __construct() {
$url = (isset($_GET['url']) ? $_GET['url'] : null);
$url = rtrim($url, '/');
$url = explode('/', $url);
if(empty($url[0])){
require'controllers/index.php';
$controller = new Index();
}else{
$file = 'controllers/'. $url[0] .'.php';
if(file_exists($file)){
require $file;
} else
die("Error");
}
}
}
This is the controller for index page
class Index extends Controller{
public function __construct() {
parent::__construct(); //Creates new view object from parents class
$this->model = parent::loadModel('index');
//there should probably exists some method that will pass the data to model
$this->view->render('index'); //renders view, but also it should have another argument that contains data, i think
if(isset($_POST['SubmitCosmonautForm'])){
var_dump($_POST);
}
}
}
This is main controller that every other controller will inherit, so all have created view object
class Controller {
public function __construct(){
$this->view = new view();
}
protected function loadModel($name){
$path = 'models/'.$name.'_model.php';
if(file_exists($path)){
require $path;
$modelName = $name.'_model';
$this->model = new modelName();
}else{
echo "Model for" .$name."doesn't exists";
}
}
}
and this is the view in library, which contains render function so far
class View{
public function render($name){
require 'views/' .$name. '.php';
}
}
For example i want to save form data to database, so when i am thinking about it i should probably pass only data to model, and inside that model i should call methods that will handle saving/loading/changing DB, these methods that operates over DB could be in database class. Is this the right approach ? i read about mvc last 2 hours but i don't know if i understand it correctly.
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');
}
}
}
My question is how to dynamically include and call the model and view classes in a simple MVC model? I have no problem calling the controller, I have found various ways to do that, but I can't find a good solution for calling and passing in the model and view.
I have setup a .htaccess file to read the url as "www.domain.com/controller/method/id".
I was previously trying to do a check if a file exists for the model and view the same way I am doing the controller using the $cont variable, and then trying to load the model and pass it into the controller, then the view. The issue I had is that all the includes are using the $cont variable to call instantiate their classes and could not tell each other apart. I tried adding a suffic $cont . 'Controller', but then I couldn't load the class at all, let alone pass in the model or view.
Here is my latest example without model or view.
<?php
//===============================================
// Debug
//===============================================
ini_set('display_errors','On');
error_reporting(E_ALL);
//===============================================
// Includes
//===============================================
require('coremvc.php');
//===============================================
// Constants & Globals
//===============================================
define('BASE_PATH', dirname(realpath(__FILE__)));
$GLOBALS['var'] = "";
//===============================================
// Session
//===============================================
session_start();
//===============================================
// Router
//===============================================
if ($_SERVER['REQUEST_URI'] !== '/') {
$uri = $_SERVER['REQUEST_URI'];
$uri = ltrim($uri, '/');
$request = explode('/', $uri);
foreach ($request as $key => $val) {
if(empty($request[$key])) {
unset($request[$key]);
}
}
$request = array_values($request);
if (isset($request[0])) {
$cont = $request[0];
}
if (isset($request[1])) {
$action = $request[1];
}
} else {
$cont = "home";
}
if (FILE_EXISTS('/controllers/' . $cont . 'Controller.php')) {
require '/controllers/' . $cont . 'Controller.php';
} else {
$cont = "home";
require '/controllers/homeController.php';
}
//===============================================
// Start the controller
//===============================================
$controller = new $cont;
I have made the following changes to the example above, posted it below, as well as my super easy bake oven simple controller.
<?php
if (FILE_EXISTS('/controllers/' . $cont . 'Controller.php')) {
require '/controllers/' . $cont . 'Controller.php';
} else {
$cont = "home";
$cont = ucfirst($cont) . 'Controller';
require '/controllers/homeController.php';
}
//===============================================
// Start the controller
//===============================================
$controller = new $cont('World');
$controller->world();
Controller, it is just extending an empty class which I was thinking I could use if I wanted to extend a common method to every class. That is what the coremvc.php is in the index.php above.
<?php
Class HomeController extends Controller
{
function __construct($world) {
echo "Hello ";
$this->world = $world;
}
function world() {
echo $this->world;
}
}
You want to load and call classes easily. I dynamically load classes that end in ".class.php". This makes things easy for me.
I put this in my index.php... where /app/ is where I have my php classes:
<?php
define('CLASS_DIR', '/app/');
set_include_path(get_include_path() . PATH_SEPARATOR . dirname(__FILE__) . CLASS_DIR);
spl_autoload_extensions('.class.php');
spl_autoload_register();
Next, I require my routes:
require 'rte/myroute.php';
I let my routes (controllers) direct my traffic to my models, albeit some parsing, etc.
I typically develop REST based APIs in PHP, so the "view" is just a JSON response.
The HTML / JavaScript client consumes the responses.
A good framework that I like is SlimPHP. I load it up using Composer.
http://www.slimframework.com/
https://getcomposer.org/
Here's an example of calling a class as an instance and statically, since you auto-loaded, you don't need to include anything at the top:
<?php
$param1 = 1;
$param2 = 2;
MyClass::myFunc($param1);
$myObj = new MyClass;
$myObj->myFunc2($param2);
As it is already say you can have a look to use the spl_autoload_register which will require all your files in the given path. You can change this function to improve the load.
Concerning the Model with your current code you can implement it as follow:
$controllerPath = "/controllers/{$cont}Controller.php";
$modelPath = "/model/{$cont}Model.php";
if (FILE_EXISTS($controllerPath)) {
require $controllerPath;
if (FILE_EXISTS($modelPath)) {
require $modelPath;
}
else {
throw new \LogicException(
sprintf("Your controller must implement a model. No model found: %s", $modelPath)
);
}
} else {
$cont = "home";
require "/controllers/{$cont}Controller.php";
require "/model/{$cont}Model.php";
}
//===============================================
// Start the controller
//===============================================
$controller = new $cont($cont);
In this sample of code, $cont is the name of the page like home. Which require the homeController and homeModel. Then in your __construct($modelName) just set the model.
However, I don't recommand you tu use this because your controller can load many Models. Then, your controller could look like this:
<?php
namespace \App\Controller; // If you use namespace
use App\Model\homeModel, // If you use namespace
App\Model\productModel; // If you use namespace
Class HomeController extends Controller
{
private $model;
/* If this function is common to all controllers just move it
in the parent Controller class( one more time, I don't recommend to set
the model using this way). */
public function __construct($model) {
$this->model= $model;
}
public function login() {
$homeModel = new homeModel(); // If you use namespace
// Example to call the view
$myValue = 'Hello world';
$this->render(array("myVariableName" => $myValue))
}
}
In this second example $this->render can be a method in your Controller class (which by the way should be an abstract class). I'll give a last sample of code for the logic of the abstract controller.
<?php
namespace \App\Controller;
abstract class AbstractController {
/* Common code for all controllers */
public function __construct() {
}
/* View renderer */
protected function render($parameters) {
/* Call the view here maybe an another class which manage the View*/
}
}
To conclude, you can implement this MVC in many way, this code is just a suggestion and maybe its not the best way. I advise you to have a look with the spl_autoload that I put at the beginning of my answer.
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!