Learn building CMS from scratch with todo list demo in PHP and MySQL

Learning a CMS is the start of professional web development. If you have already used some CMSs such as Wordpress, Drupal, Joomla, etc then you will already know what a CMS is, how they look and how they work. However, even if you have used none of those CMSs before you need not worry as we will not be using elements of those systems.

What is a CMS?

CMS stands for Content Management System. It is a way to manage content (your data) on a server. Using a CMS makes your code modular, with web pages created dynamically. A CMS makes code much more sensible and readable. For example, in a dynamic web page, we have a header (the top section of the page), sidebars, a footer (bottom section), content from a database (most things are stored in the database except files) and some functions & classes for handling the web page such as manipulating database, form validations and security handler. For more read CMS vs MVC frameworks.

Requirements:

  • For this application, you must have a basic knowledge of HTML, CSS, and PHP. I will also use very basic jquery. If you know nothing about jquery, then you will still be able to understand this series.
  • An install of LAMP, WAMP or XAMPP according to your operating system.
  • PHP 5.4+
  • Bootstrap (we will download it later, you need not to know anything about it. It is a pre-written collection of CSS and js files that provide a liquid layout and essential theme. It will reduce our time so that we can put our focus on PHP).

If you are very new with PHP then use my step by step PHP tutorials.

Application:

In this article I will make a To-Do Application with the following functions:

  • Log in, Log out and Register users
  • Create a todo entry under different labels
  • Save a due date and calculate the remaining time
  • Show the progress of the work
  • Edit and delete any entry
  • Admin Section for handling themes and users

The application will have the following features:

  • Portability: Just like WordPress, it can have custom themes and widgets.
  • Perfect distribution: It will be very modular with different files and folders.
  • Secure: Here we are building a very small application but we will consider many security issues for best-developing practice.
  • Admin interface: In our case, the admin interface is just for managing themes and users, but you can extend it by adding more functions.
  • Pretty links (Routing): Just like an MVC framework, this CMS has a routing system.

The code is open source and available in my Github repository. If you don't want to copy the code from here, then you can download a zip file from there. Live demo is at TODO LIVE DEMO. Admin section is at ADMIN SECTION.

Demo credentials:

email: harish@flowkl.com

password: harish123

What can you do after learning from this series?

  1. Creating a dynamic website
  2. Distribute code in files and folders.
  3. Security using CSRF token, escaping special parameters, regex matching (preg_match function), and MySQL prepare statements.
  4. Create routing
  5. Using bootstrap
Screen shot of To DO dashboard

Create a directory and files as shown here in folder www or htdoc according to your operating system. My file structure is:

todolist directory

Take a good look at this structure. In the root folder todolist, there are six folders and one file.

Don't worry if you do not understand any point. We will discuss this in further detail later.

  1. todolist is my root folder (root folder of my application).
  2. There is a .htaccess file in the root folder (todolist folder). This file is redirecting all requests from browser to index.php file (in todolist folder).
  3. All configurations are defined in config.php file.
  4. index.php is the file that receives all requests and generates dynamic web pages as a response.
  5. The includes folder is the base folder, i.e., all base files are present in the includes folder. All other files extend the functions of these files. Base.php is the base file of all files. This file will be extended by Application.php, Widget.php, and TemplateFunctions.php.
  6. The application folder contains all the application files, i.e., the files which are written for any application. In our case, Todo is an application. Every application extends Application.php.
  7. The libraries folder contains the library files (general required files). For example, database-related files.
  8. The templates folder contains the themes and static files. Template files use the functions defined in TemplateFunction.php.
  9. The widget folder has widget modules. All widgets will extend Widget.php. In this folder, we will write our widgets.
  10. The general static files which are independent of any theme are stored in the asset folder.

Ignore the .gitignore, README.md, LICENSE, cms_todo.sql, and config.example.php files. These files are for sending code on Github.

How does this CMS work?

As mentioned before, the .htaccess file will redirect all requests to index.php file in the root folder. First, the index.php file will define the security variable which will be checked by each file so that no-one can access any other file directly. In other words, all other files (except static files) can be opened only by the index.php file.

Then configurations from the config.php file is fetched and stored in constants so that other files can use them. After that, we define a session variable with the name CSRF. This variable will be verified when we submit any form. It ensures that the form submission request is coming from a web page that is served by our website.

After this, we call the TemplateFunction class from the TemplateFunction.php file. TemplateFunction registers the theme and includes an index.php file from the theme folder (theme folders are in the templates folder). So how are web pages generated in the index.php file inside themes? We will learn this in the series.

Prepare the .htaccess file

The .htaccess file is used by the server. It provides a way to make configuration changes on a per-directory basis. The requests which are passed to the todolist directory will be filtered by this file (as it is placed in the todolist folder). The code of this file is:

  1. RewriteEngine on
  2. RewriteCond %{REQUEST_FILENAME} !-d
  3. RewriteCond %{REQUEST_FILENAME} !-f
  4. RewriteCond $1 !^(images|photos|css|js|robots\.txt)
  5. RewriteRule ^(.+)$ index.php/$1 [NC,L]

NOTE: This file only works when rewrite mode is turned on.

The first line is telling us that rewrite mode is turned on. The second and third lines are collecting all requests for files or directories. The fourth line removes those requests which are static requests. The fifth line is sending all other requests to the index.php file. For more detail on the .htaccess file read the Apache official documents.

Set configurations in config.php file

Now write these configurations:

  1. <?php
  2. return [
  3. // set a database connection as default
  4. // assign the name defined in connection variable
  5. 'database' => 'mysql', // mysql is connection name, can be anything
  6. // config database connections
  7. 'connection' => [
  8. // name of this connection, used in above option only
  9. 'mysql' => [
  10. 'driver' => 'mysql', //database type
  11. 'host' => 'your host', //database host name, on local server it is 'localhost'
  12. 'database' => 'your database name', // database name
  13. 'username' => 'your username', // database username
  14. 'password' => 'your password', // user password
  15. ],
  16. ],
  17. // set the location of application withour trailing slash
  18. // in my case it is 'http://localhost/todolist'
  19. 'root' => '', // example 'http://localhost/todolist' or http://demo.findalltogeher.com
  20. // register apps which are defined in application folder
  21. 'apps' => [
  22. 'auth',
  23. 'todo',
  24. 'admin',
  25. ],
  26. // secret string for encryption
  27. 'secret' => 'this_is_secret_string', // example 'fa&k+j@sdf!has^dh-iu!d#dh$sd'
  28. ];

This code is well commented. Why is an array of connections defined instead of using one connection? It is a port for multiple database connections. I am using it because I am using the same code in both local and production servers. I only have to change the value of the ‘database’ to correct the connection. We will register our application as ‘apps’. Here we are defining three applications:

  • auth: It will handle the authentication (login, log out, registration, reset password).
  • todo: This is our application :)
  • admin: This is our admin section.

We will create these apps in future tutorials. Note that this file has no closing tag for PHP (?>). This is to remove extra white space as the PHP tag does not need to be closed.

Create index.php in root:

Now we will handle requests using index.php. First close displaying error messages and define a security variable:

  1. <?php
  2. if (ini_get('display_errors')) {
  3. ini_set('display_errors', '0');
  4. }
  5. $security_check = 1;

Lines 3-5 will stop displaying errors. In development (on your local server), it is good to show displaying errors so that you can track the debug. So, in the local server change the above code with:

  1. <?php
  2. if (!ini_get('display_errors')) {
  3. ini_set('display_errors', '1');
  4. }
  5. $security_check = 1;

$security_check is a variable which we will check in our files.

Include config.php

Now add this code in index.php file:

  1. <?php
  2. // define configurations
  3. $config = require_once('config.php');
  4. // define root folder
  5. define('ROOT', $config['root']);
  6. // define database
  7. $database_config = $config['connection'][$config['database']];
  8. $driver = $database_config['driver'];
  9. define('DATABASE', ucfirst($driver).'Database');
  10. define('DATABASE_NAME', $database_config['database']);
  11. define('DATABASE_USER', $database_config['username']);
  12. define('DATABASE_PASS', $database_config['password']);
  13. define('DATABASE_SERVER', $database_config['host']);
  14. // register apps
  15. $GLOBALS['APPS'] = $config['apps'];
  16. // define production secret key
  17. define('SECRET', $config['secret']);

In line 1, we are including config.php which returns all configurations as an array. In line number 4, we are defining a constant named ROOT. In lines 9-14, we are defining constants for the database.

Add CSRF token:

Now add this code for adding CSRF.

  1. <?php
  2. session_start();
  3. if(isset($_SESSION['CSRF'])!== true)
  4. {
  5. // all character which can be in random string
  6. $characters = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ';
  7. $charactersLength = strlen($characters);
  8. $randomString = '';
  9. for ($i = 0; $i < 32; $i++) {
  10. // choosing one character from all characters and adding it to random string
  11. $randomString .= $characters[rand(0, $charactersLength - 1)];
  12. }
  13. // store generated csrf token using sha512 hashing
  14. $_SESSION['CSRF'] = hash('sha512',time().''.$randomString);
  15. }
  16. // make CSRF as constant for using in templates
  17. define('CSRF', $_SESSION['CSRF']);
  18. // if request is not GET then verify csrf_token
  19. if($_SERVER['REQUEST_METHOD']!=='GET')
  20. {
  21. if(!isset($_REQUEST['csrf_token']) || $_REQUEST['csrf_token'] !== $_SESSION['CSRF'])
  22. {
  23. // if wrong csrf_token stop user from accessing
  24. header('HTTP/1.1 403 Forbidden');
  25. exit;
  26. }
  27. }

We are starting a session for the user (if the user has already a session the old session will continue). Then we are checking if the user session has a variable with the name CSRF. If not, then we are generating a 32 character long random string and hashing this string with time using the hash() function. After this, we are storing the token in a constant. Lines 21-29 are for verifying requests in form submission cases. Up to this point our index.php file is:

  1. <?php
  2. if (ini_get('display_errors')) {
  3. ini_set('display_errors', '0');
  4. }
  5. $security_check = 1;
  6. // define configurations
  7. $config = require_once('config.php');
  8. // define root folder
  9. define('ROOT', $config['root']);
  10. // define database
  11. $database_config = $config['connection'][$config['database']];
  12. $driver = $database_config['driver'];
  13. define('DATABASE', ucfirst($driver).'Database');
  14. define('DATABASE_NAME', $database_config['database']);
  15. define('DATABASE_USER', $database_config['username']);
  16. define('DATABASE_PASS', $database_config['password']);
  17. define('DATABASE_SERVER', $database_config['host']);
  18. // register apps
  19. $GLOBALS['APPS'] = $config['apps'];
  20. // define production secret key
  21. define('SECRET', $config['secret']);
  22. session_start();
  23. if(isset($_SESSION['CSRF'])!== true)
  24. {
  25. $characters = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ';
  26. $charactersLength = strlen($characters);
  27. $randomString = '';
  28. for ($i = 0; $i < 32; $i++) {
  29. $randomString .= $characters[rand(0, $charactersLength - 1)];
  30. }
  31. $_SESSION['CSRF'] = hash('sha512',time().''.$randomString);
  32. }
  33. define('CSRF', $_SESSION['CSRF']);
  34. if($_SERVER['REQUEST_METHOD']!=='GET')
  35. {
  36. if(!isset($_REQUEST['csrf_token']) || $_REQUEST['csrf_token'] !== $_SESSION['CSRF'])
  37. {
  38. header('HTTP/1.1 403 Forbidden');
  39. exit;
  40. }
  41. }

In this part, we will write one of two library classes: MysqlDatabase and Route. MysqlDatabase handles the mysql database queries and Route handles the routing.

MysqlDatabase class:

Location of mysql database file

Create a file mysql_database.php in folder todolist/libraries/core/database. In this file first, we will check our security variable. Then we will write a class named MysqlDatabase. This class has the following functions:

  1. Set configuration for a connection
  2. Connection to a database
  3. Disconnect from database
  4. Quote data before putting it in the database. It is used to make sensitive data secure before putting in the database. We will use it for email addresses. For other data, we will use other methods.
  5. execute a prepare statement for insertion and deletion in the database.
  6. Select a single row from the database and return it
  7. Select a set of rows from the database and return them.

Note: I am using only prepare statements for all database queries. Functions query() and prepare() do the same work but in a different manner. query() embeds variables in it directly and executes statements for the database but prepare() first binds variables to the sql query. In other words, we make blocks in SQL statements and prepare assign variables to those blocks and execute later. Now if a user fills data something like this: username = 'username' and password = 'something' OR A = A. The query function will allow the user for all values for password because it is taking three parameters username, password and A ( A = A returns true and depresses password check). But the prepare function will notice that we want only two parameters and will assign the value 'something' OR A = A to password instead of creating one more parameter for A.

prepare function is also fast in performance than the query function. So, I will highly recommend you to use prepare function over query as much as possible. prepare has one more advantage that it sends SQL statement only once and after that, it sends only variables. It is useful when you have to perform the same action multiple times. It will reduce your bandwidth and also give you a high speed.

Security check:

We want that only root index.php file can open this file, so we will add this code before defining our class:

  1. <?php
  2. // security check
  3. if(!isset($security_check))
  4. {
  5. echo "This is restricted file";
  6. exit();
  7. }

It wil check if there is a variable with name $security_check or not. We are defining this variable only in index.php. If anyone access this file directly then $security_check will be absent and file will be closed.

Now define MysqlDatabase class:

  1. /**
  2. * class for mysql database
  3. */
  4. class MysqlDatabase
  5. {
  6. // defining secrete variables
  7. private $db_name= DATABASE_NAME;
  8. private $db_user= DATABASE_USER;
  9. private $db_pass= DATABASE_PASS;
  10. private $db_server= DATABASE_SERVER;
  11. // protected PDO connection
  12. protected $con;

Here I have defined four private and one protected variables. Private variables are our server config data and the protected variable is our database connection.

Set private variables:

If we have multiple databases, then we need to change the value of private variables. So, we are writing a function for it.

  1. /**
  2. * setting database configuration
  3. */
  4. function set_config($server,$db,$user,$pass)
  5. {
  6. $this->db_server=$server;
  7. $this->db_name=$db;
  8. $this->db_user=$user;
  9. $this->db_pass=$pass;
  10. }

Connect to MySQL database:

Now we have to connect our $con variable with the database. I am using PDO for it.

  1. /**
  2. * connecting with a mysql database
  3. */
  4. private function connect()
  5. {
  6. $info = 'mysql:host='.$this->db_server.';dbname='.$this->db_name;
  7. try{
  8. $this->con = new PDO($info, $this->db_user, $this->db_pass);//data server connection
  9. }
  10. catch(PDOException $Exception )
  11. {
  12. header('HTTP/1.1 500 Database Error');
  13. exit;
  14. }
  15. if (!$this->con)
  16. {
  17. die('Could not connect: ' . mysql_error());
  18. }
  19. }

In connect function we are opening a connection with MySQL server using PDO. This function is private because I will call this function from this class only. I prefer PDO over mysql_connect and mysqli because of its overall performance. Mysqli is more fast than PDO, but it has limited functions and especially only limited up to MySQL. It is always good to put PDO statements in try catch block so it can not display default errors which may contain your private data.

Disconnect from the database:

Now we need one more function which will disconnect the connection. This function is also private.

  1. /**
  2. * disconnecting database connection
  3. */
  4. private function disconnect()
  5. {
  6. $this->con = null;
  7. }

Why do we need to disconnect the database manually? It is because it will close the database connection just after using it. There will not be any vain connection. It will raise the performance by reducing the load of the database server.

Quote data before placing in database:

Placing sensitive data in a database is insecure. quote() function puts the quote around the parameter assign to it. It is helpful in reducing risks in SQL injection.

  1. /**
  2. * quoting string
  3. * @param string $args
  4. * @return sting $args
  5. */
  6. function quote($arg)
  7. {
  8. $this->connect();
  9. $arg = $this->con->quote($arg);
  10. $this->disconnect();
  11. return $arg;
  12. }

Here first, we are connecting to a database in line 8. In line 9, we are quoting the data. In line 10, the connection is closed and in line 11, quoted data is returned.

Execute a prepare statement:

  1. /**
  2. * prepare statement for single fetch
  3. * @param string $sql
  4. * @param array $args
  5. * @return query $q
  6. */
  7. function prepare($sql,$args)
  8. {
  9. $this->connect();
  10. $q =$this->con->prepare($sql);
  11. $q ->execute($args);
  12. $this->disconnect();
  13. return $q;
  14. }

prepare() function takes two parameters. First is a SQL query, and another is an array of values for binding with this query. This function is only for one way queries like insertion and deletion in a database. Just like quote() function, we are opening the database connection then preparing a database query. Then binding the values to query. After it, we are disconnecting the database connection, and finally, we return the query. Why do we return a query? Because after execution we want to verify if database transaction is successful or failed. If database transaction is failed then $q will be null.

Load data from database:

We will load data from the database just like prepare() function. But the only change will be that we will write one more line which will fetch data from the database. Mysql has two functions fetch() and fetchAll() which fetch data from the database. fetch() fetches only single row and fetchAll() can fetch multiple rows.

  1. /**
  2. * Return array of results
  3. * @param string $sql
  4. * @param array $args
  5. * @return query array $rows
  6. */
  7. function load_result($sql,$args)
  8. {
  9. $this->connect();
  10. $q = $this->con->prepare($sql);
  11. $q->execute($args);
  12. $rows = $q->fetchAll();
  13. $this->disconnect();
  14. return $rows;
  15. }
  16. /**
  17. * Return single result
  18. * @param string $sql
  19. * @param array $args
  20. * @return query $row
  21. */
  22. function load_single_result($sql,$args)
  23. {
  24. $this->connect();
  25. $q = $this->con->prepare($sql);
  26. $q->execute($args);
  27. $row = $q->fetch();
  28. $this->disconnect();
  29. return $row;
  30. }
  31. }
  32. ?>

load_single_result() returns only a single row and load_result() returns multiple rows. What will happen if there is no row in the database corresponding to the query? Then these functions will return null (empty) value. And finally, we are closing our class. Here I have a closing PHP tag (?>). You can ignore it.

Final mysql_database.php:

The finally our class is:

  1. <?php
  2. /**
  3. * @author Harish Kumar
  4. * @copyright flowkl
  5. * @link http://www.flowkl.com
  6. * @version 1.0
  7. * This is core file which manupulate database instructions
  8. * It is using PDO connection
  9. * MAll functions are using prepare statements
  10. */
  11. // security check
  12. if(!isset($security_check))
  13. {
  14. echo "This is restricted file";
  15. exit();
  16. }
  17. /**
  18. * class for mysql database
  19. */
  20. class MysqlDatabase
  21. {
  22. // defining secrete variables
  23. private $db_name= DATABASE_NAME;
  24. private $db_user= DATABASE_USER;
  25. private $db_pass= DATABASE_PASS;
  26. private $db_server= DATABASE_SERVER;
  27. // protected PDO connection
  28. protected $con;
  29. /**
  30. * setting database configuration
  31. */
  32. function set_config($server,$db,$user,$pass)
  33. {
  34. $this->db_server=$server;
  35. $this->db_name=$db;
  36. $this->db_user=$user;
  37. $this->db_pass=$pass;
  38. }
  39. /**
  40. * connecting with a mysql database
  41. */
  42. private function connect()
  43. {
  44. $info = 'mysql:host='.$this->db_server.';dbname='.$this->db_name;
  45. try{
  46. $this->con = new PDO($info, $this->db_user, $this->db_pass);//data server connection
  47. }
  48. catch(PDOException $Exception )
  49. {
  50. header('HTTP/1.1 500 Database Error');
  51. exit;
  52. }
  53. if (!$this->con)
  54. {
  55. die('Could not connect: ' . mysql_error());
  56. }
  57. }
  58. /**
  59. * disconnecting database connection
  60. */
  61. private function disconnect()
  62. {
  63. $this->con = null;
  64. }
  65. /**
  66. * quoting string
  67. * @param string $args
  68. * @return sting $args
  69. */
  70. function quote($arg)
  71. {
  72. $this->connect();
  73. $arg = $this->con->quote($arg);
  74. $this->disconnect();
  75. return $arg;
  76. }
  77. /**
  78. * prepare statement for single fetch
  79. * @param string $sql
  80. * @param array $args
  81. * @return query $q
  82. */
  83. function prepare($sql,$args)
  84. {
  85. $this->connect();
  86. $q =$this->con->prepare($sql);
  87. $q ->execute($args);
  88. $this->disconnect();
  89. return $q;
  90. }
  91. /**
  92. * Return array of results
  93. * @param string $sql
  94. * @param array $args
  95. * @return query array $rows
  96. */
  97. function load_result($sql,$args)
  98. {
  99. $this->connect();
  100. $q = $this->con->prepare($sql);
  101. $q->execute($args);
  102. $rows = $q->fetchAll();
  103. $this->disconnect();
  104. return $rows;
  105. }
  106. /**
  107. * Return single result
  108. * @param string $sql
  109. * @param array $args
  110. * @return query $row
  111. */
  112. function load_single_result($sql,$args)
  113. {
  114. $this->connect();
  115. $q = $this->con->prepare($sql);
  116. $q->execute($args);
  117. $row = $q->fetch();
  118. $this->disconnect();
  119. return $row;
  120. }
  121. }
  122. ?>

Now, we will create a routing library. Why do we need a routing library, and what does it do? As we are sending each request to a single file (index.php in root folder), we need to identify the requested URL so we can do a proper task. We will define URLs for each application in this form:

  1. <?php
  2. return [
  3. 'auth/login-form' => [
  4. 'name' => 'login-form',
  5. 'control' => 'auth@login_form',
  6. 'allow' => ['GET','POST']],
  7. ];
  8. ];
Php cms directory for applications in to do demo

Php cms directory for applications in todo demo

We will store this code in a file named as a route.php inside each application folder. This file is returning an array. This array contains key and value pairs. Keys are the regex of URLs (part after ROOT domain). Regex is a special string which represent/ filter a limited types of strings. Read more about regex on Wikipedia. In above example, 'auth/login-form' is representing the url 'http://domain-name/auth/login-form'. In my case, it is 'http://localhost/todolist/auth/login-form'. The value of this key is also an array. It contains three key-value pairs.

  1. Name: It is the name of that URL. It can be any string and must be unique for each URL. We will call this url with its name.
  2. Control: Control is the function that will handle the action for that URL. auth@login_form means the login_form function of the Auth class. We will write this class inside the folder todolist/applications/auth.]
  3. Allow: Allow is a filter that will restrict only specific types of requests. In the above case, we are permitting 'GET' and 'POST' type requests only.

If you are not introduced with MVC framework then you will be wondering why we are using so complex structure? We can just map urls with functions. But what will happen when we want to change our function name or our url for any task? It will be a headache because we must have to change in all code. But by assigning a name to url, urls are independent from function and vice versa. So, we can change url any time and our code will not be effective because we will use the name of the urls everywhere.

But if you still want to just map urls with functions, then you can use a low-level mapping.

  1. <?php
  2. return [
  3. 'auth/login-form' => 'auth@login_form',
  4. ];

Here we are directly adding regex of url with functions directly.

Overview of Route class:

Libraries in php cms for routing

Libraries in PHP cms for routing

Now it is clear what we want from our routing library. We will define a class in todolist/libraries/core/http/routes.php file. In this file, first, we check the security variable, and then we will define our class Route. Route class will have the following functions:

  • Collect all routes from each app and store then in an array. In our case, we have three apps auth, admin and todo.
  • Return the destination (name of class and function, stored with control key) of an url.
  • Return from the name of the url's regex, i.e., take the name of the url as parameter and search the url and return the url.

Capturing parameters in route:

We are mapping functions with urls but function may require some arguments from users which is a part of url itself. For example, we want to show the description of a single to-do item:

  1. <?php
  2. 'todo/show/{id}[0-9]+' => [
  3. 'name' => 'show-todo',
  4. 'control' =>'todo@show',
  5. 'allow' => ['GET']
  6. ],

Here we want to capture the id of a to-do from url. For example, we want to capture 1, 11 and 21 respectively from these urls:

  • http://localhost/todolist/todo/show/1
  • http://localhost/todolist/todo/show/11
  • http://localhost/todolist/todo/show/21

Now we introduced a new syntax {id}[0-9]+. Here [0-9]+ is a regex syntax which means only non-negative integer numbers are allowed. Before regex expression we added {id}. It means that we want to capture the value of this regex and store it in a variable named id. Our Route class will filter such urls and will return captured variables to map functions.

Start routes.php file

So, starting of our todolist/libraries/core/http/routes.php file is:

  1. <?php
  2. // security check
  3. if(!isset($security_check))
  4. {
  5. echo "This is restricted file";
  6. exit();
  7. }
  8. class Route
  9. {
  10. private $routes;

$routes is the array which will store all routes from all apps.

Load routes:

Now load all routes in $routes variable.

  1. <?php
  2. /**
  3. * load all routes for applications
  4. */
  5. function load_routes()
  6. {
  7. $apps = $GLOBALS['APPS'];
  8. foreach ($apps as $app) {
  9. foreach (require('applications/'.$app.'/routes.php') as $regex => $control) {
  10. $this->routes[$regex] = $control;
  11. }
  12. }
  13. }

If you remembered, we have stored all apps in a global variable in index.php file. Here we are iterating on each app and finding the routes.php file in that app. Line 7 is iterating over each app and line 8 is iterating over each route inside the routes.php file. In line 9, we are adding each route to $routes array. In other words, we are fetching all arrays from different routes.php files and storing them in a single array $routes. I have designed it for a case when we have a number of routes in each app. If you have very few routes, then you can store them in a single file.

Get destination:

Now we want to know the destination, the function which will handle the route. We have mapped this function in our route with a key control. We have to just return the value of this key. But before returning the value, we have to find out the right route. But we can not do it straightforward because we have also added one more feature of capturing parameters in urls. So, we have to do four tasks:

  1. get requested url
  2. Filter each regex and remove all parameters.
  3. Compare filtered regex with requested url
    • If url matches, then check if regex requested type allows or not
    • If request is valid, then store all captures parameters in an array ($args)
  4. return destination along with $args

Note that we won't only the final destination. So the first three functions must be private.

Get requested url:

  1. <?php
  2. /**
  3. * get requested url
  4. * @return string $url
  5. */
  6. private function get_url()
  7. {
  8. $trimed = trim(split('\?',$_SERVER['REQUEST_URI'])[0],'/');
  9. $url = split('/',$trimed);
  10. $number_of_folders = count(split('/', ROOT))-3;
  11. for ($i=0; $i < $number_of_folders; $i++) {
  12. array_shift($url);
  13. }
  14. $url = join("/",$url);
  15. return $url;
  16. }

$_SERVER['REQUEST_URI'] returns the current requested url. In line 7, we are doing two things:

  1. First, we are splitting url with '?' and selecting the first part, i.e., we are removing get parameters.
  2. Then we are trimming trailing slash.

For example: after this operation, if requested url is http://www.domain.com/todolist/auth/?parm=1&args;=0" class="redactor-linkify-object">http://www.domain.com/todolist/auth/?parm=1&args;=0 then $trimed will be http://www.domain.com/todolist/auth (no trailing slash and no get parameters).

We have defined regex regarding our root folder which may be inside a folder. In line 8, url is split and stored in an array. Above link will be split like this array('http:', '', 'www.domain.com','todolist','auth'). In line 10, we are splitting ROOT constant. If you ROOT constant is 'http://www.domain.com/todolist' then split array will be array('http:', '', 'www.domain.com', 'todolist'). count() function returns the length of the array, which is 4 in our case. If the domain name is the root of the CMS, then the count will be 3 otherwise it will add the number of extra folders (1 in my case). Because we want url relative to ROOT so we will remove extra folders. In line 12, array_shift() is removing the first element of $url in each iteration.

Finally, we are joining $url and returning url relative to ROOT.

Filter regex:

We have all regex and also url. We want to match url with proper regex but we have named parameters ({name}) in our regex. In this section we will remove such parameters and store those parameters along with their position number in arrays.

  1. <?php
  2. /**
  3. * filter parameters from url's regex
  4. * @param string $regex
  5. * @return array (string $regex) (array $args) (array $arg_pos)
  6. */
  7. private function filter_regex($regex)
  8. {
  9. $args = array();
  10. $arg_pos = array();
  11. $regex_array = split('/',$regex);
  12. foreach ($regex_array as $pos => $r)
  13. {
  14. // if there is a dynamic parameter in url
  15. if (strpos($r,'{') !== false)
  16. {
  17. $perm = substr($r,strpos($r,'{')+1,strpos($r,'}')-strpos($r,'{')-1);
  18. array_push($args, $perm);
  19. array_push($arg_pos, $pos);
  20. $regex_array[$pos] = str_replace( '{'.$perm.'}', '', $r );
  21. }
  22. }
  23. $regex = join('/',$regex_array);
  24. return ['regex' =>$regex, 'args' => $args, 'arg_pos' => $arg_pos];
  25. }

We want to remove all {name} from our regex string. For example, if our regex string is 'todo/show/{id}[0-9]+' then we want only 'todo/show/[0-9]+'. We are splitting regex with '/'. In each part of regex if we find '{' then we have to remove whole sub string {name}. In line 15, we are checking for '{' and if we found '{' then in line 17, we are storing name in $perm. In line 18, we are storing names in $args, and in line 19, we storing the position of a name in $arg_pos. In line 20, the name from the regex has been removed along with '{' and '}'. For reading more on string operations read String in PHP - part 1 and String in PHP - part -2.

Finally, we are joining changed regex_array. This function is returning one string (filtered regex) and two arrays (one is an array of names and another is their position in url).

Check regex:

Now we have requested url, filtered regex, and named parameters along with their positions. It is time to validate the url against the regexes.

  1. <?php
  2. /**
  3. * match current url with registered url regexes
  4. */
  5. private function check_regex()
  6. {
  7. $url = $this->get_url();
  8. foreach ($this->routes as $route => $url_meta) {
  9. // filter the regex string
  10. $filtered_regex = $this->filter_regex($route);
  11. // matich regex
  12. preg_match('#^'.$filtered_regex['regex'].'$#',$url, $matches, PREG_OFFSET_CAPTURE);
  13. if(!empty($matches))
  14. {
  15. if(is_array($url_meta))
  16. {
  17. $control = $url_meta['control'];
  18. if( array_search($_SERVER['REQUEST_METHOD'],$url_meta['allow']) === false)
  19. {
  20. return false;
  21. }
  22. }
  23. else
  24. $control = $url_meta;
  25. $args = array();
  26. if(!empty($filtered_regex['args']))
  27. {
  28. $url_split = split('/', $url);
  29. $pos = 0;
  30. foreach ($filtered_regex['args'] as $arg) {
  31. $args[$arg] = $url_split[$filtered_regex['arg_pos'][$pos++]];
  32. }
  33. }
  34. return ['control' => $control,'args' => $args];
  35. }
  36. }
  37. return false;
  38. }

In line 5, we are obtaining current request url. After it in line 10, we are filtering each route. In line 13, preg_match() compare route's regex and url. $matches will we assign the result (true or false) of the comparison.

If url is valid (url finds its regex), then we are checking if the value of that regex is a string or an array. In other words, we are checking if the name and allow indexes are used or not (not in case of low-level mapping as we discussed earlier). If yes, then we are checking if the requested method is valid or not. We are also initializing $control with destination function. In line 27, we have defined an array. If filtered regex has arguments, then we will push them in $args array.

Finally, if we find proper regex corresponding to requested url then destination address (class name and function name) is returned otherwise false is returned.

Get Destination:

Finally, we have to make a public function which return destination.

  1. <?php
  2. /**
  3. * find right app class and its funciton
  4. * @return array $destination
  5. */
  6. function get_destination()
  7. {
  8. $destination = $this->check_regex();
  9. if($destination!==false)
  10. {
  11. return $destination;
  12. }
  13. header('HTTP/1.1 404 Page not found');
  14. exit;
  15. }

This function is finding the proper regex for url and sending destination.

Get url from route name:

Our final work is to obtain url from the name. For example, if we called the name login-form then we get 'auth/login-form'. For this task, we need a function with two parameters. The first parameter is the name of the route and another is the array of values that have to insert in url (in case of regex with named parameters). But for routes with parameters, we need to map values to the url's regex places. So, we need one more function which will assign values to each named parameter in the regex.

  1. <?php
  2. /**
  3. * map values to parameters in url regex
  4. */
  5. private function map_url($regex, $args)
  6. {
  7. $regex_array = split('/', $regex);
  8. foreach ($args as $k => $v) {
  9. $pos=0;
  10. foreach ($regex_array as $r) {
  11. if(strpos($r, '{'.$k.'}') !== false)
  12. {
  13. $regex_array[$pos] = $v;
  14. }
  15. $pos++;
  16. }
  17. }
  18. return join($regex_array,'/');
  19. }
  20. /**
  21. * return a named url
  22. * @return string url
  23. */
  24. public function to_route($name,$args = null)
  25. {
  26. foreach ($this->routes as $regex => $meta)
  27. {
  28. if(is_array($meta))
  29. {
  30. if($name === $meta['name'])
  31. {
  32. if($args!==null)
  33. return ROOT.'/'.$this->map_url($regex, $args);
  34. else
  35. return ROOT.'/'.$regex;
  36. }
  37. }
  38. }
  39. }
  40. }

Here to_route() function matches given route name in parameter against each route's name. If name matches with any route, line 32 will check if there are arguments for that route. If there is $args then first map_url() will fill those routes and then absolute url will be returned otherwise directly absolute url will be returned.

Final routes.php file:

Our final class is:

  1. <?php
  2. /**
  3. * @author Harish Kumar
  4. * @copyright Find All Together
  5. * @link http://www.findalltogeher.com
  6. * @version 1.0
  7. * This class handles the routes comming from browsers
  8. * These routes are permalink routes
  9. */
  10. // security check
  11. if(!isset($security_check))
  12. {
  13. echo "This is restricted file";
  14. exit();
  15. }
  16. class Route
  17. {
  18. private $routes;
  19. /**
  20. * load all routes for applications
  21. */
  22. function load_routes()
  23. {
  24. $apps = $GLOBALS['APPS'];
  25. foreach ($apps as $app) {
  26. foreach (require('applications/'.$app.'/routes.php') as $regex => $control) {
  27. $this->routes[$regex] = $control;
  28. }
  29. }
  30. }
  31. /**
  32. * get requested url
  33. * @return string $url
  34. */
  35. private function get_url()
  36. {
  37. $trimed = trim(split('\?',$_SERVER['REQUEST_URI'])[0],'/');
  38. $url = split('/',$trimed);
  39. $number_of_folders = count(split('/', ROOT))-3;
  40. for ($i=0; $i < $number_of_folders; $i++) {
  41. array_shift($url);
  42. }
  43. $url = join("/",$url);
  44. return $url;
  45. }
  46. /**
  47. * filter parameters from url's regex
  48. * @param string $regex
  49. * @return array (string $regex) (array $args) (array $arg_pos)
  50. */
  51. private function filter_regex($regex)
  52. {
  53. $args = array();
  54. $arg_pos = array();
  55. $regex_array = split('/',$regex);
  56. foreach ($regex_array as $pos => $r)
  57. {
  58. // if there is a dynamic parameter in url
  59. if (strpos($r,'{') !== false)
  60. {
  61. $perm = substr($r,strpos($r,'{')+1,strpos($r,'}')-strpos($r,'{')-1);
  62. array_push($args, $perm);
  63. array_push($arg_pos, $pos);
  64. $regex_array[$pos] = str_replace( '{'.$perm.'}', '', $r );
  65. }
  66. }
  67. $regex = join('/',$regex_array);
  68. return ['regex' =>$regex, 'args' => $args, 'arg_pos' => $arg_pos];
  69. }
  70. /**
  71. * match current url with registered url regexes
  72. */
  73. private function check_regex()
  74. {
  75. $url = $this->get_url();
  76. foreach ($this->routes as $route => $url_meta) {
  77. // filter the regex string
  78. $filtered_regex = $this->filter_regex($route);
  79. // matich regex
  80. preg_match('#^'.$filtered_regex['regex'].'$#',$url, $matches, PREG_OFFSET_CAPTURE);
  81. if(!empty($matches))
  82. {
  83. if(is_array($url_meta))
  84. {
  85. $control = $url_meta['control'];
  86. if( array_search($_SERVER['REQUEST_METHOD'],$url_meta['allow']) === false)
  87. {
  88. return false;
  89. }
  90. }
  91. else
  92. $control = $url_meta;
  93. $args = array();
  94. if(!empty($filtered_regex['args']))
  95. {
  96. $url_split = split('/', $url);
  97. $pos = 0;
  98. foreach ($filtered_regex['args'] as $arg) {
  99. $args[$arg] = $url_split[$filtered_regex['arg_pos'][$pos++]];
  100. }
  101. }
  102. return ['control' => $control,'args' => $args];
  103. }
  104. }
  105. return false;
  106. }
  107. /**
  108. * find right app class and its funciton
  109. * @return array $destination
  110. */
  111. function get_destination()
  112. {
  113. $destination = $this->check_regex();
  114. if($destination!==false)
  115. {
  116. return $destination;
  117. }
  118. header('HTTP/1.1 404 Page not found');
  119. exit;
  120. }
  121. /**
  122. * map values to parameters in url regex
  123. */
  124. private function map_url($regex, $args)
  125. {
  126. $regex_array = split('/', $regex);
  127. foreach ($args as $k => $v) {
  128. $pos=0;
  129. foreach ($regex_array as $r) {
  130. if(strpos($r, '{'.$k.'}') !== false)
  131. {
  132. $regex_array[$pos] = $v;
  133. }
  134. $pos++;
  135. }
  136. }
  137. return join($regex_array,'/');
  138. }
  139. /**
  140. * return a named url
  141. * @return string url
  142. */
  143. public function to_route($name,$args = null)
  144. {
  145. foreach ($this->routes as $regex => $meta)
  146. {
  147. if(is_array($meta))
  148. {
  149. if($name === $meta['name'])
  150. {
  151. if($args!==null)
  152. return ROOT.'/'.$this->map_url($regex, $args);
  153. else
  154. return ROOT.'/'.$regex;
  155. }
  156. }
  157. }
  158. }
  159. }

In this section, we will make the interface of your CMS. We will write our core files in the includes directory. First, we will make a Base.php file which contains an abstract Base class. This class will be extended by other core classes. Whatever we will define here will be available in our whole CMS.

File in included directory | PHP CMS from scratch

There are three more core files: Application.php, Widget.php, and TemplateFunctions.php. Each of these files has one class (Application, Widget, and TemplateFunction respectively). These classes are extending the Base class. TemplateFunction is the class which handles the flow of the CMS. It integrates all other parts. Application is another abstract class that is extended by our applications (in applications folder). Widget class is also an abstract class which is used to write new widgets.

Read more about abstract classes or inheritance in PHP.

Base class:

In todolist/includes create a file Base.php. In this file, first, we will check our security variable. Then we include core libraries and finally we will write our Base class. Let's start :)

  1. <?php
  2. if(!isset($security_check))
  3. {
  4. echo "This is restricted directory";
  5. exit();
  6. }
  7. require_once('libraries/core/database/'.$driver.'_database.php');
  8. require_once('libraries/core/http/routes.php');
  9. abstract class Base{

Here we are including Mysql_database.php and routes.php files. We have defined $driver variable in root index.php. It is available here because we are including this file via index.php file.

We need five things in our whole CMS:

  1. A database connection
  2. Check if the user is logged in or not
  3. Array of all routes from routes.php files
  4. Get urls using their names
  5. Run application for output

Including the same files and classes, again and again, is an overheat. So, we need not to make instances for database and routes every time. These variables must be static. Our Base class is very simple and does not need much explanations.

Database connection:

  1. <?php
  2. /**
  3. * create a instance of database class
  4. * Open a database commenction
  5. * @return instance $db_object MysqlDatabase
  6. */
  7. public function get_dbo()
  8. {
  9. static $db_object = null;
  10. if (null === $db_object) {
  11. $db = DATABASE;
  12. $db_object = new $db();
  13. }
  14. return $db_object;
  15. }

Here I am using OOP concepts, if you are not familiar with OOP then please read OOP tutorials.

In line 8, I am defining a static database connection. In the next line, I am checking if it is null or not. It will be null only the first time. If it is null then I am creating an instance of database class (MysqlDatabase class).

Check if the user is authenticated or not:

  1. <?php
  2. /**
  3. * chcek if user is logged in
  4. * @return boolean
  5. */
  6. protected function is_login()
  7. {
  8. if(isset($_SESSION['user_session']))
  9. {
  10. return true;
  11. }
  12. return false;
  13. }

I am authenticated user using sessions. $_SESSION is an array of the variable stored in session. We will define 'user_session' in auth application. $_SESSION['user_session'] is defined only for logged in user. If user is logged in, then is_login() will return true else false.

Load all routes in a variable:

  1. <?php
  2. /**
  3. * get an instance of Route class
  4. * load all routes
  5. * @return instance $route Routes()
  6. */
  7. protected function get_routes()
  8. {
  9. static $routes = null;
  10. if (null === $routes) {
  11. $routes = new Route();
  12. $routes->load_routes();
  13. }
  14. return $routes;
  15. }

Similar to get_dbo() function, get_routes() is also setting and returning a static variable. This function will load all routes from all routes.php files to $routes variable.

Get url from names:

  1. <?php
  2. /**
  3. * it is used for named urls
  4. * @param string $name
  5. * @param array $args
  6. * @return string url
  7. */
  8. protected function to_url($name, $args=null)
  9. {
  10. $r = $this->get_routes();
  11. if($args!==null)
  12. return $r->to_route($name,$args);
  13. else
  14. return $r->to_route($name);
  15. }

This function takes the name of an url and returns then absolute url.

Abstract run function:

Finally, we have an abstract function which will be called for execution.

  1. <?php
  2. /**
  3. * abatract function run for executing applicaiton
  4. */
  5. abstract function run($arg = null, $param = null);
  6. }

Our complete file looks like this code on Github.

Render first web page:

It is time to test our application till now.

  • Create a folder "default" in todolist/templates directory and a file index.php inside it.
  • Create a new file in todolist/includes with name TemplateFunctions.php.

What are we going to do?

  • We will execute TemplateFunctions class from root index.php file.
  • TemplateFunction class will return index.php file from todolist/templates/default directory.

For adding TemplateFunctions class add these lines in the end of todolist/index.php.

  1. <?php
  2. // include template
  3. require_once('includes/TemplateFunctions.php');
  4. $tmpl=new TemplateFunctions();
  5. $tmpl->run();

Here we are just calling run() function of TemplateFunctions class. Open todolist/includes/TemplateFunctions.php and paste this code. Code is explained in comments.

  1. <?php
  2. /**
  3. * This is base template controller file.
  4. * The purpose of this file is to provide php functions to the template files
  5. * This file contains helpers for template files.
  6. * All CMS template management related functions will be here.
  7. */
  8. require_once('Base.php');
  9. class TemplateFunctions extends Base{
  10. // default theme
  11. private $template_name = null;
  12. /**
  13. * initialize template
  14. */
  15. function __construct()
  16. {
  17. if($this->template_name == null)
  18. {
  19. $this->template_name = 'default';
  20. }
  21. }
  22. /**
  23. * run whole CMS and render generated html page
  24. */
  25. function run($arg = null, $param = null)
  26. {
  27. require_once('templates/default/index.php');
  28. }
  29. }

Finally, in todolist/templates/default/index.php add:

  1. welcome this is first page

Now open link http://localhost/todolist :) We have rendered first page. But it is not what we want. We have to create a complete dynamic application.

Create template:

In a dynamic webpage has various components like header, sidebars, footer, and main content. We can write all of the components in a file but for the large web pages (web page with 1000+ lines of codes), it is messy to manage all things in one file. It is especially when you are writing a very custom webpage (web page with a number of logic and conditions). For example, you are using different widgets in the sidebar for different types of pages. So, we are distributing code in files. Because our application is very small, you will not feel the need for such distribution. But In professional development, you will always need it.

Create these files in todolist/templates/default directory:

  • header.php
  • sidebar.php
  • footer.php

Header:

In header.php paste this code:

  1. <?php
  2. /**
  3. * This is header of the default theme.
  4. * THis header contains logo, main menu and search box in the top section of the web page.
  5. * @author Harish Kumar
  6. * @copyright Flowkl
  7. * @link https://www.flowkl.com
  8. * @version 1.0
  9. */
  10. ?><!DOCTYPE html>
  11. <html>
  12. <head>
  13. <title>MyCMS from screech</title>
  14. <meta name="viewport" content="width=device-width, initial-scale=1">
  15. <link type='text/css' rel='stylesheet' href='<?php echo STATIC_PATH;?>css/bootstrap.min.css' />
  16. <link type='text/css' rel='stylesheet' href='<?php echo STATIC_PATH;?>css/style.css' />
  17. <script type="text/javascript" src="<?php echo STATIC_PATH;?>js/jquery-2.1.1.min.js"></script>
  18. </head>
  19. <body>
  20. <div class="hfeed site">
  21. <div id="header" class="header col-sm-12">
  22. <header id="masthead" class="site-header" role="banner">
  23. <div class="site-branding">
  24. <h1 class="site-title"><a href="<?php echo ROOT; ?>" rel="home">My To Do List</a></h1>
  25. </div><!-- .site-branding -->
  26. </header><!-- .site-header -->
  27. </div><!-- .header -->
  28. <div class="container-fluid">

Here you can see that we have used one undefined constant STATIC_PATH.

Footer:

In footer.php paste this code:

  1. <div class="footer col-lg-12">
  2. 2015 © <a class="credit-link" href="https://www.flowkl.com/wp/about-us">Harish Kumar</a> @ <a class="credit-link" href="https://www.flowkl.com/">Find all together| programming and Web Development</a>
  3. </div>
  4. </div>
  5. <script type="text/javascript" src="<?php echo STATIC_PATH;?>js/bootstrap.min.js"></script>
  6. </body>
  7. </html>

Static files:

We are using three css (we will use one later) and two js files. Make two folders css and js in the default folder. Download these files from here: STATIC FILES. Extract it and put css files in css folder and js files in js folder.

Update index.php

Now integrate all parts. Edit todolist/templates/default/index.php as:

  1. <?php $this->get_header(); ?>
  2. <div class='content col-md-8 col-lg-10'>
  3. Here we will push our content
  4. </div>
  5. <div class="sidebar col-lg-1"><?php $this->get_sidebar(); ?></div>
  6. </div>
  7. <div class="clear"></div>
  8. <?php
  9. $this->get_footer(); ?>

TemplateFunctions class:

We are using some more functions in our templates like get_header(). Now define them. Change TemplateFunctions.php as:

  1. <?php
  2. /**
  3. * @author Harish Kumar
  4. * @copyright Flowkl
  5. * @link https://www.flowkl.com
  6. * @version 1.0
  7. * This is base template controller file.
  8. * The purpose of this file is to provide php functions to the template files
  9. * This file contains helpers for template files.
  10. * All CMS template management related functions will be here.
  11. */
  12. require_once('Base.php');
  13. class TemplateFunctions extends Base{
  14. // default theme
  15. private $template_name = null;
  16. // hook for widgets
  17. private $widget_positions=array();
  18. /**
  19. * initialize template
  20. */
  21. function __construct()
  22. {
  23. if($this->template_name == null)
  24. {
  25. $this->template_name = $this->get_current_template();
  26. }
  27. }
  28. /**
  29. * run whole CMS and render generated html page
  30. */
  31. function run($arg = null, $param = null)
  32. {
  33. require_once(TEMPLATE_PATH.'index.php');
  34. }
  35. /**
  36. * set a template
  37. */
  38. function set_template($template_name)
  39. {
  40. $this->template_name=$template_name;
  41. }
  42. /**
  43. * get currently active template
  44. */
  45. private function get_current_template()
  46. {
  47. // we will improve this function later
  48. return 'default';
  49. }
  50. /**
  51. * return current theme name
  52. * @return string $this->template_name
  53. */
  54. function get_current_theme()
  55. {
  56. return $this->template_name;
  57. }
  58. /**
  59. * return currently active template path
  60. * it is relative path to root index.php
  61. * @return string template_path
  62. */
  63. function get_current_template_path()
  64. {
  65. return 'templates/'.$this->template_name.'/';
  66. }
  67. /**
  68. * return static media path in template path
  69. * it is absolute path
  70. * @return string template_path
  71. */
  72. function get_static_path()
  73. {
  74. return ROOT.'/'.TEMPLATE_PATH;
  75. }
  76. /**
  77. * return header.php file from current template
  78. */
  79. function get_header()
  80. {
  81. require_once(TEMPLATE_PATH.'header.php');
  82. }
  83. /**
  84. * return footer.php file from current template
  85. */
  86. function get_footer()
  87. {
  88. require_once(TEMPLATE_PATH.'footer.php');
  89. }
  90. /**
  91. * return sidebar.php file from current template
  92. */
  93. function get_sidebar()
  94. {
  95. require_once(TEMPLATE_PATH.'sidebar.php');
  96. }
  97. }

The code is well explained in comments. Now the only unknown things are TEMPLATE_PATH and STATIC_PATH.

Improve index.php

Now replace last line of todolist/index.php,i.e, $tmpl->run(); with it:

  1. <?php
  2. define('TEMPLATE_PATH', $tmpl->get_current_template_path());
  3. define('STATIC_PATH', $tmpl->get_static_path());
  4. define('THEME', $tmpl->get_current_theme());
  5. $tmpl->run();

Here we are defining constants. Now refresh the page: http://localhost/todolist. You will find a basic theme :). 

As it is a very long tutorial, I need to split it into two parts. In the next part, we will write an interface for our applications and also our first application for authentication.

Theme after 4th tutorials | create php cms from scratch


ReactJS with Redux Online Training by Edureka

About Harish Kumar

Harish, a fullstack developer at www.lyflink.com with five year experience in full stack web and mobile development, spends most of his time on coding, reading, analysing and curiously following businesses environments. He is a non-graduate alumni from IIT Roorkee, Computer Science and frequently writes on both technical and non-technical topics.

Related Articles

With the expanding market of mobile apps, the developers are struggling to maintain the code bases for Native apps. M...
5 Elite and Imperative Hybrid App Frameworks
Django is a great framework in python. But all of the hosting do not provide django hosting in their shared or free h...
Cheap Django Hosting
Laravel provides blade template. Blade files are similar to php files and cover all features of php files. In additio...
Blade Template in Laravel 7

Complete Python Bootcamp: Go from zero to hero in Python 3

Top Posts

Recent Posts

The Complete Web Developer Course - Build 25 Websites

Meet on Twitter

Subscribe

Subscribe now and get weekly updates directly in your inbox!

Any Course on Udemy in $6 (INR 455)

Development Category (English)300x250

Best Udemy Courses

PHP with Laravel for beginners - Become a Master in Laravel