├── .gitignore ├── templates ├── dummy.php └── base.php ├── static └── css │ └── app.css ├── LICENSE ├── index.php ├── core ├── router.php └── util.php └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | *.db 2 | -------------------------------------------------------------------------------- /templates/dummy.php: -------------------------------------------------------------------------------- 1 | 8 | * @license MIT 9 | */ 10 | ?> 11 | This is some dummy content. -------------------------------------------------------------------------------- /static/css/app.css: -------------------------------------------------------------------------------- 1 | /** 2 | * app.css 3 | * 4 | * @author Prahlad Yeri 5 | * @license MIT 6 | */ 7 | div.main 8 | { 9 | font-family: courier; 10 | font-size: 21px; 11 | background-color: white; 12 | margin: 75px 200px 75px 200px; 13 | padding: 12px 10px 12px 10px; 14 | border: 2px solid black; 15 | border-radius: 20px; 16 | min-height: 400px; 17 | } 18 | 19 | body { 20 | background-color: lightgrey; 21 | } 22 | 23 | .strong {font-weight: bold;} -------------------------------------------------------------------------------- /templates/base.php: -------------------------------------------------------------------------------- 1 | 8 | * @license MIT 9 | */ 10 | ?> 11 | 12 | 13 | 14 | <?=$title?> 15 | 16 | 17 | 18 |
19 |

Welcome to minimal-mvc. 20 |

21 | This is an attempt to remove the usual cruft that goes around the monstrous PHP frameworks and other software components in webdev world. 22 |

23 | Together, we will create a more frugal and elegant world without cruft, enjoy! 24 |

25 | 26 |
27 | 28 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Prahlad Yeri 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /index.php: -------------------------------------------------------------------------------- 1 | 8 | * @license MIT 9 | */ 10 | //@todo: add docstrings for functions 11 | require('core/router.php'); 12 | require('core/util.php'); 13 | session_start(); 14 | 15 | //$base_url = "http://localhost:8000/src/"; //for easy access to static paths 16 | //$index_file = 'index.php'; //set blank if you rewrite index.php in .htaccess 17 | 18 | //@todo: initialize constants and vars 19 | const VERSION = "0.1"; 20 | 21 | //@todo: initialize database 22 | $dbh = new PDO("sqlite:minimal-mvc.db"); 23 | 24 | function index() { 25 | //echo uri_segment(1) ."
"; 26 | //echo "segments::" . print_r(uri_segments(), true) . "
"; 27 | echo "

It Works!

"; 28 | } 29 | 30 | function arcane(){ 31 | echo "

Pattern Match!

"; 32 | echo "

The uri segments are :".print_r(uri_segments(),true)."

"; 33 | } 34 | 35 | function testmvc() { 36 | $vars = ["foo"=>'bar', 'title'=>'Testing']; 37 | load_template('templates/dummy.php', $vars); 38 | } 39 | 40 | function testform(){ 41 | echo "
"; 42 | } 43 | 44 | dispatch(function(){ 45 | //check install and any other common initialization 46 | //global $dbh; 47 | error_log("pre-dispatch:"); 48 | }); -------------------------------------------------------------------------------- /core/router.php: -------------------------------------------------------------------------------- 1 | 8 | * @license MIT 9 | */ 10 | const MMVC_VER = "1.0"; 11 | define('APP_PATH', dirname(__DIR__ )); 12 | $base_url = null; 13 | $index_file = 'index.php'; 14 | //$routes = array(); 15 | 16 | function site_url($uri = "") { 17 | $idx = ($index_file==''?'' : $index_file .'/'); 18 | return base_url() . $idx . $uri; 19 | } 20 | 21 | function base_url() { 22 | global $base_url; 23 | return $base_url; 24 | } 25 | 26 | 27 | function dispatch($pre_dispatch_func) { 28 | //$method = $_SERVER['REQUEST_METHOD']; 29 | global $base_url, $index_file; 30 | $uri = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH); 31 | if ($base_url === null) { //set it automatically 32 | //echo "setting base_url automatically"; 33 | $base_url = "http://" . $_SERVER['HTTP_HOST']; 34 | $base_url .= str_replace(basename($_SERVER['SCRIPT_NAME']),"",$_SERVER['SCRIPT_NAME']); 35 | error_log("index file is " . $index_file); 36 | error_log( "auto setting base_url as ".$base_url); 37 | } else { //strip off from uri 38 | //@todo: validate base_url is proper 39 | $turi = parse_url($base_url, PHP_URL_PATH); 40 | $turi= rtrim($turi, '/'); 41 | //echo "uri:$uri
turi$turi
"; 42 | $uri = substr($uri, strlen($turi)); 43 | //echo "uri:$uri
"; 44 | } 45 | if (strpos($uri, "/index.php") === 0) { 46 | $uri = substr($uri, 10); 47 | } 48 | if ($uri=="") $uri = "/"; 49 | if (is_callable($pre_dispatch_func)) { 50 | $call = $pre_dispatch_func; 51 | $call(); 52 | } 53 | //echo 'uri_segment[0]::' . uri_segment(1); 54 | //error_log( "uri::$uri"); 55 | //@todo: ensure a way of limiting total segments, lenghty urls like /foo/bar/baz/xyz/1234 56 | // ideal way is to let functions have their parameters, then pass it on to them after a count validation. 57 | if (uri_segment(1) == '' && function_exists('index')) { 58 | index(); 59 | } else if (function_exists(uri_segment(1))) { 60 | $func = uri_segment(1); 61 | call_user_func($func, $uri); 62 | } else { 63 | http_response_code(404); 64 | echo '404 Not Found'; 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /core/util.php: -------------------------------------------------------------------------------- 1 | 8 | * @license MIT 9 | */ 10 | 11 | /* MISC UTILITIES */ 12 | function get_method() { 13 | return $_SERVER["REQUEST_METHOD"]; 14 | } 15 | function default_vars($module) { 16 | $vars = ["module"=>$module, 17 | 'errors'=>[], 18 | "messages"=>[], 19 | ]; 20 | //error_log("default_vars::SESSION" . print_r($_SESSION,true)); 21 | if (isset($_SESSION['message'])) { 22 | $vars['messages'][] = $_SESSION['message']; 23 | unset($_SESSION['message']); 24 | } 25 | if (isset($_SESSION['error'])) { 26 | $vars['errors'][] = $_SESSION['error']; 27 | unset($_SESSION['error']); 28 | } 29 | return $vars; 30 | } 31 | 32 | /* DATABASE UTILITIES */ 33 | function exec_sql($dbh, $sql, $vals) { 34 | $sth = $dbh->prepare($sql); 35 | $sth->execute($vals); 36 | return true; 37 | } 38 | function fetch_rows($dbh, $sql, $arr=null) { 39 | $sth = $dbh->prepare($sql); 40 | if ($arr) 41 | $sth->execute($arr); 42 | else 43 | $sth->execute(); 44 | return $sth->fetchAll(); 45 | } 46 | function build_insert_query($table, $values) 47 | { 48 | $fields = []; 49 | $padding = []; 50 | $value_array = []; 51 | foreach($values as $key=>$value) { 52 | //if (!is_numeric($value)) $value = "'".$value."'"; 53 | $fields[] = $key; 54 | $padding[] = ":$key"; 55 | $value_array[] = $value; 56 | } 57 | return [ "insert into $table(" . implode(',', $fields) . ") values(" . implode(',', $padding) . ")", 58 | $value_array ]; 59 | } 60 | function build_update_query($table, $values, $id) 61 | { 62 | $ss = "update ".$table." set "; 63 | $fields = []; 64 | $value_array = []; 65 | foreach($values as $key=>$value) { 66 | //if (!is_numeric($value)) $value = "'".$value."'"; 67 | $fields[] = "$key=:$key"; 68 | $value_array[] = $value; 69 | } 70 | $fields[] = "modified_at=?"; 71 | $value_array[] = (new DateTime())->format("Y-m-d H:i:s"); 72 | $where = "id=:id"; 73 | $value_array[] = $id; 74 | return [ $ss . implode(',', $fields) . " where " . $where, 75 | $value_array ]; 76 | } 77 | function clean_sql($sql) { 78 | $parts = explode(";", $sql); 79 | $rval = ""; 80 | for($i=0; $i 0) { 93 | // $rval .= $ss . ";"; 94 | // echo "CLEAN_SQL:". $ss . ":".strlen($ss) . "
"; 95 | // } 96 | } 97 | return $rval; 98 | }; 99 | 100 | /* URL UTILITIES */ 101 | 102 | function load_template($fname, $vars) { 103 | extract($vars); 104 | //echo "base_url::".base_url(); 105 | $__content_file = $fname; 106 | require("templates/base.php"); 107 | } 108 | 109 | function get_uri() { 110 | //$bup = parse_url($base_url, PHP_URL_PATH); 111 | $uri = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH); 112 | if (strpos($uri, "/index.php") === 0) { 113 | $uri = substr($uri, 10); 114 | } 115 | return $uri; 116 | } 117 | 118 | function uri_qs() { 119 | $parts = parse_url($_SERVER['REQUEST_URI']); 120 | if (isset($parts["query"])) { 121 | $out = []; 122 | parse_str($parts["query"], $out); 123 | return $out; 124 | } 125 | else { 126 | return null; 127 | } 128 | } 129 | 130 | function uri_segment($idx) { 131 | $uri = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH); 132 | $buri = parse_url(base_url(), PHP_URL_PATH); 133 | error_log("full-uri::$uri"); 134 | error_log("buri::$buri"); 135 | $uri = substr($uri, strlen($buri)-1); 136 | error_log("proper-uri::$uri"); 137 | if (strpos($uri, "/index.php") === 0) { 138 | $uri = substr($uri, 10); 139 | } 140 | $parts = explode('/', $uri); 141 | //print_r($parts); 142 | if ($idx<0) $idx = count($parts)+$idx; 143 | return $parts[$idx]; 144 | } 145 | 146 | 147 | function uri_segments() { 148 | $uri = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH); 149 | $buri = parse_url(base_url(), PHP_URL_PATH); 150 | $uri = substr($uri, strlen($buri)-1); 151 | if (strpos($uri, "/index.php") === 0) { 152 | $uri = substr($uri, 10); 153 | } 154 | return explode('/', $uri); 155 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![license](https://img.shields.io/github/license/prahladyeri/minimal-mvc.svg) 2 | ![last-commit](https://img.shields.io/github/last-commit/prahladyeri/minimal-mvc.svg) 3 | [![patreon](https://img.shields.io/badge/Patreon-brown.svg?logo=patreon)](https://www.patreon.com/prahladyeri) 4 | [![paypal](https://img.shields.io/badge/PayPal-blue.svg?logo=paypal)](https://paypal.me/prahladyeri) 5 | [![follow](https://img.shields.io/twitter/follow/prahladyeri.svg?style=social)](https://twitter.com/prahladyeri) 6 | 7 | **minimal-mvc** is a humble attempt to de-cruft and de-bloat the scene of web frameworks. It's a specialized micro framework for following use cases or traits: 8 | 9 | 1. Freelancers, students and hobbyists who want to experiment with PHP. 10 | 2. Your app has simple CRUD workflow and just needs basic routing and templating capabilities of PHP. 11 | 3. Your app is mostly frontend heavy (SPA, etc.) and uses PHP for very basic features like routing. 12 | 4. You are developing a REST API. 13 | 5. You find it unnecessary to optimize for hypothetical futuristic scaling. 14 | 6. Not a huge fan of applying OOP everywhere. 15 | 7. Generally prefer to work with core language capabilities than hand-holding of a heavy framework. 16 | 17 | **How to use minimal-mvc framework:** 18 | 19 | Just download this repo and use it to prototype your app. The core consists of only two PHP scripts which are required in index.php: 20 | 21 | - `core/router.php` - For routing capabilities. 22 | - `core/util.php` - For generic utility functions. 23 | 24 | In `index.php`, you can handle basic routing easily like this: 25 | 26 | ```php 27 | function index() { 28 | echo "

It Works!

"; 29 | }; 30 | ``` 31 | 32 | This is a very simple routing arrangement where each function inside index.php is a route with the `index()` function being the main or default route. For example, `/` routes to `index()`, `/api` routes to `api()`, etc. 33 | 34 | ```php 35 | function api() { 36 | echo "

Pattern Match!

"; // http://localhost/api/foo 37 | echo "

The uri segments are :".print_r(uri_segments(),true)."

"; 38 | }; 39 | ``` 40 | 41 | You can know the current HTTP method by `get_method()` utility function and get individual route segments using the `uri_segment()` utility function (such as 'api' in case of `uri_segment(1)` where route is `/api/foo/bar`). Similarly, `uri_segments()` returns an array consisting of all route segments. 42 | 43 | For views/templates, you can use the load_template() utility function as shown in this built-in example: 44 | 45 | ```php 46 | function testmvc() { 47 | $vars = ["foo"=>'bar', 'title'=>'Testing']; 48 | load_template('templates/dummy.php', $vars); 49 | }; 50 | ``` 51 | 52 | The template system works on a stereotype base template (`templates/base.php`) which can include all the frontend details like link and script tags to bootstrap, react, jquery, etc. And it should contain a placeholder called `$__contentfile` somewhere in the body section for the contents of "child template" (such as the built-in `templates/dummy.php` template) which is derived or inherited from the base template and directly passed in the `load_template()` utility function. In that child template, all variables you pass (`$vars` in this example) will be extrapolated for you to use. Note that we will not use any specific template language like `jinja` or `twig` as PHP itself is a template engine. 53 | 54 | In addition to that, the framework also includes a static directory to store your static files like stylesheets, ECMA scripts, images, etc. 55 | 56 | Other useful utility functions are `base_url()` and `site_url()`. These are useful for resolving full url paths when your app is hosted inside a sub folder like `http:///subfolder` or when you want to resolve the actual url from a route such as "foo/bar". For almost everything else under the Sun, PHP is more than capable of handling whatever you throw at it. 57 | 58 | The routing capability provided here is very basic, any further implementation will be a DIY. Other frameworks provide fancy routes like `/foo/bar/{slug}` and `/article/{locale}` which appears mind-blowing initially. But once you consider that PHP provides you a built-in called `$_SERVER['REQUEST_URI']` which you can parse yourself inside the `/foo/bar/` or `/article/` routes to get these yourself, that magic starts waning! To make things a bit easier, this framework provides you the shortcut utility function `uri_segment()` as mentioned earlier to determine these so called fancy variables: 59 | 60 | ```php 61 | echo uri_segment(3); // outputs the {slug} value or third segment in the URI 62 | ``` 63 | 64 | **What Next?** 65 | 66 | The util.php is a work in progress and will keep improving with time. The idea is really that simple, PHP was originally built as a language that employed functions to manage its workflow (to a great extent, it still does), and minimal-mvc is also in the same spirit. If your app increases in complexity or scale, you can put the controller logic inside additional script modules and require them in index.php like this: 67 | 68 | ```php 69 | function foo() { 70 | require_once("controllers/foo_controller.php"); 71 | }; 72 | ``` 73 | 74 | It's upto you whether you name that folder controllers or something else, whether you use classes inside the script or plain old functions. As I said, there will be no hand holding! --------------------------------------------------------------------------------