I've got some experience with Laravel but for fun I started a small project (basically a CRUD with also Google Maps) without a framework because I considered that is oversized.
This is the directory structure.
+---assets
| +---css
| +---img
| \---js
+---classes
+---inc
| +---templates
| \---views
| \---partials
+---libs
\---pages
- index.php
- config.php
- initialize.php
- .htaccess
- robots.txt
In pages folder there are the equivalent of MVC Controllers, every file is related to a precise url (route-like).
In .htaccess
I re-wrote all requests to index.php
except for the assets folder and robots.txt.
RewriteEngine on
RewriteCond $1 !^(index\.php|assets|robots\.txt)
RewriteRule ^(.*)$ index.php/$1 [L]
This is the index.php
:
<?php
require_once 'initialize.php';
//slashes for trim() function
$slashes = '\/';
//get the request uri without slashes and query string
$uri = trim(strtok($_SERVER['REQUEST_URI'],'?'), $slashes);
//get the base url from config var
$base_url = config_get('base_url');
//if needed, remove the base url from request uri
if (!empty($base_url) && (substr($uri, 0, strlen($base_url)) == $base_url)) {
$uri = substr($uri, strlen($base_url));
$uri = trim($uri, $slashes);
}
// empty request uri = home page
if(empty($uri)){
$view = new View('home', 'main');
$view->setTitle('Home')->render();
}else{
//routing basic
switch ($uri){
case 'bts/create':
require PAGES_PATH . 'bts_add.php';
break;
case 'bts/create/save':
require_once PAGES_PATH . 'bts_add_save.php';
break;
case 'bts/edit':
require_once PAGES_PATH . 'bts_edit.php';
break;
case 'map':
require_once PAGES_PATH . 'map.php';
break;
case 'map/api':
require_once PAGES_PATH . 'map_api.php';
break;
default:
show_404();
break;
}
}
Here initialize.php
:
<?php
session_start();
require_once 'config.php';
foreach (glob(APP_PATH . '/libs/*.php') as $file){
require_once $file;
}
spl_autoload_register(function($class){
$path = APP_PATH . '/classes/' . $class . '.php';
if(file_exists($path)){
require_once $path;
}
});
set_exception_handler(function($e){
if (ob_get_level()){
ob_end_clean();
}
$page = <<<HTML
<!doctype html>
<head>
<title>App error</title>
</head>
<body>
<h1>There is a problem</h1>
<p>{$e->getMessage()}</p>
<p><strong>File:</strong> {$e->getFile()}</p>
<p><strong>Line:</strong> {$e->getLine()}</p>
</body>
HTML;
@http_response_code(500);
die($page);
});
Config.php:
<?php
define('APP_PATH', __DIR__);
define('PAGES_PATH', APP_PATH . '/pages/' );
define('INC_PATH', APP_PATH . '/inc/' );
$config = array(
'base_url' => 'my_project/cells',
'db' => array(
'host' => 'localhost',
'username' => 'root',
'password' => '',
'db_name' => 'cell_database'
),
);
Basic functions in libs folder:
<?php
/**
* Shows 404 page with HTTP status code
*/
function show_404(){
http_response_code(404);
$view = new View('404', 'main');
$view->setTitle('Not found');
$view->render();
die();
}
/**
*
* @global array $config
* @param string $key
* @return mixed
*
* Returns a value from config array
*/
function config_get($key){
global $config;
return (isset($config[$key]) ? $config[$key] : null);
}
/**
*
* @param string $path
* @param array $qs - The query string (optional)
* @return string
*
* Generates full url from given path
*/
function url($path, $qs = array()){
$path = trim($path, '\/');
$url = ('//' . $_SERVER['HTTP_HOST'] . '/' . (config_get('base_url') ? config_get('base_url') . '/' : '') . $path);
if(is_array($qs) && $qs){
$url .= '?' . http_build_query($qs, '&');
}
return $url;
}
/**
* returns full url of an asset resource
*
* @param string $file
* @return string
*/
function asset($file){
return url('assets/' . $file);
}
/**
*
* @param string $url
*
* Redirects to given url
*/
function redirect($url){
http_response_code(301);
header("Location: $url");
die();
}
View Class:
<?php
/**
* Class for view rendering
*
*/
class View {
const GMAP_SCRIPT = '<script async defer
src="https://maps.googleapis.com/maps/api/js?key=blabla&libraries=places&callback=initMap">
</script>';
protected $view, $template, $title, $variables = [], $js = [], $css = [], $gmap = false;
/**
*
* @param string $view - view name
* @param string $template - template name
* @param array $variables - index array of variables
*/
public function __construct($view, $template, $variables = []) {
$this->view = INC_PATH . 'views/' . $view . '.php';
$this->template = INC_PATH . 'templates/' . $template . '.php';
$this->variables = $variables;
}
/**
*
* @param string $title - page title
* @return \View
*/
public function setTitle($title) {
$this->title = $title;
return $this;
}
/**
*
* @param array $js - array of javascrips
* @return \View
*/
public function setJs($js) {
$this->js = $js;
return $this;
}
/**
*
* @param array $css - array of css
* @return \View
*/
public function setCss($css) {
$this->css = $css;
return $this;
}
/**
* Includes gmap script
*
* @return \View
*/
public function includeGmap(){
$this->gmap = true;
return $this;
}
/**
* Render the view and send to output buffer
*
* @param array $variables
*/
public function render() {
extract($this->variables);
ob_start();
if (isset($this->template) && is_file($this->template)) {
require_once $this->template;
} else {
throw new Exception('Template is invalid');
}
ob_end_flush();
}
}
DB singleton class.
<?php
Class Db{
private static $pdo;
public static function getConnection(){
if(!isset(self::$pdo)){
self::$pdo = new PDO('mysql:dbname='. config_get('db')['db_name'].';host=' . config_get('db')['host'], config_get('db')['username'], config_get('db')['password']);
}
return self::$pdo;
}
private function __construct(){}
private function __clone() {}
}
My questions are:
- Is MVC pattern suggested also in small PHP projects?
- If not, is my app structure good or bad?
- If bad, what to do to make it better?