Coding a PHP 7 Framework #2 - Code Workshop

In part 2 of Coding a PHP Framework, we create and explore the all important configuration files.



Coding a PHP 7 Framework #2 - Code Workshop

16th August 2018 in   PHP Tutorials by Elliott Barratt

In this series of articles, we'll walk through the coding of a PHP micro-framework. It is assumed you are fairly familiar with PHP and coding / developing in general, however we'll try to explain each step so that a novice could follow.

You can view the finished framework at any time by visiting the GitHub repo at https://github.com/erbarratt/lectric.

If you need more space to see the code, click "Hide Sidebar >" above the Article Categories box to the right ->

The /engine/config.php file

The configuration file is arguably the most important file of a framework like this one (save perhaps for the /.htaccess file, without which nothing would work!). In it, we'll set up how the application will function, including:

  • How to handle strings in our PHP script.
  • Define various important variables such as the Document Root.
  • Instantiate a database connection.
  • Route to an application specific configuration file.
  • Define how we want to report errors by default.
  • Chop up the URL into something more useful.
  • Define how we would like our framework to autoload class files.
  • Start the session if required.

Here is the full file output:

<?php

	/* 
	* String function settings 
	*/
		mb_internal_encoding('UTF-8');	// Tell PHP that we're using UTF-8 strings until the end of the script use mb_ for string functions...
		mb_http_output('UTF-8'); 		// Tell PHP that we'll be outputting UTF-8 to the browser 
		
		
	/* 
	* Check version 
	*/
		if (PHP_MAJOR_VERSION  < 7){
			echo '<p style="text-align:center;">This framework only supports PHP 7.1 ></p>';
		}
		
	
	/*
	* set up directory root
	*/
		define('DOC_ROOT',dirname(__DIR__));
	
	
	/**
	* grab the application specific configuration eg. dbatase connection, doc root and definition list dbatase connection, doc root and definition list
	*/
		if (file_exists(DOC_ROOT.'/engine/app_config.php')) {
			require(DOC_ROOT.'/engine/app_config.php');
		}
		
		
	/*
	* set up display warnings for debug, defaults to true
	* default to error reporting on, unless core_config debug definition overrides in /engine/plugin/core_config
	*/
		if (!defined('DEBUG')){
			define ('DEBUG', true);
		}
		
		if (DEBUG){
			error_reporting(E_ALL);
			ini_set('display_errors', '1');
		}
		
		
	/*
	* Default constants - override these in /engine/app_config.php
	*/
		if (!defined('SITE_NAME')){ define('SITE_NAME','Lectric'); }									//for ,eta title
		if (!defined('SITE_LINK')){ define('SITE_LINK',$_SERVER['SERVER_NAME']); } 						//url, defaults to nothing
		if (!defined('SITE_DESCRIPTION')){ define('SITE_DESCRIPTION','Lectric Default Installation'); }	//for meta desc
		if (!defined('DEFAULT_DIRECTORY')){ define('DEFAULT_DIRECTORY','default'); }					//for view directory selection
		if (!defined('SESSION_IGNORES')){ define('SESSION_IGNORES', []); }								//if scripts need to set own headers, they can be ignored for seesion start further down.
		if (!defined('VIEW_ON_FAILED_DO_REQUEST')){ define('VIEW_ON_FAILED_DO_REQUEST', true); }		//how do we deal with bad URL /do/ requests? View or not...
		
		
	/*
	* define base URL NODES, URL_REQUEST AND REQUEST_METHOD
	*/
		//base request
		define ('URL_REQUEST', $_SERVER['REQUEST_URI']);
		define('URL_PATH', parse_url(URL_REQUEST, PHP_URL_PATH));
		define ('REQUEST_METHOD', $_SERVER['REQUEST_METHOD']);
		
		//re-register get params
		define('REQUEST_QUERY_STRING', parse_url(URL_REQUEST, PHP_URL_QUERY));
		mb_parse_str(REQUEST_QUERY_STRING, $_GET);

		//set up nodes
		$lecNodes = explode('/', trim(URL_PATH, '/')); //trim important for URL_NODES index numbers
		$lecNodes = array_filter( $lecNodes, function($value) { return $value !== ''; });
		define('URL_NODES', $lecNodes);
	
		
	
	/*
	* define autoloader for lectric classes
	*/
		function lecAutoload($className) :void
		{
			
			$classnameBits = explode('\\', $className);
			
			//lectric Library
				if(count($classnameBits) === 2){
					if (file_exists(DOC_ROOT.'/library/'. $classnameBits[0] .'/'. $classnameBits[1] .'.class.php')){
						include_once(DOC_ROOT.'/library/'. $classnameBits[0] .'/'. $classnameBits[1] .'.class.php');
					}
				}
			return;

		}
		spl_autoload_register('lecAutoload');
		
	/**
	* grab the Composer vendor autloader if it exists
	*/
		if (file_exists(DOC_ROOT.'/vendor/autoload.php')) {
			require(DOC_ROOT.'/vendor/autoload.php');
		}
		
	
	/*
	* get the database connection from core_config, or set to null (if DB not needed)
	*/
		if (defined('DB_HOST') && defined('DB_NAME') && defined('DB_USER') && defined('DB_PASSWORD')){
			try { 
				$lecDBH = new \PDO('mysql:host='.DB_HOST.';dbname='.DB_NAME.';charset=utf8mb4', DB_USER, DB_PASSWORD);
				$lecDBH->setAttribute(\PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
				$lecDBH->setAttribute(\PDO::MYSQL_ATTR_USE_BUFFERED_QUERY, FALSE);
				$lecDBH->setAttribute(\PDO::ATTR_STRINGIFY_FETCHES, false);
				//return database data type instead of all strings
				$lecDBH->setAttribute(\PDO::ATTR_EMULATE_PREPARES, false);
			}  
			catch(PDOException $e) {  
				echo $e->getMessage(); exit;
			} 
		} else {
			$lecDBH = null;
		}
		
		
	/* 
	* SESSION 
	*/
		if (( !in_array(trim(URL_REQUEST,'/'), SESSION_IGNORES) )){
			if (session_status() == PHP_SESSION_NONE) {
				session_start();
			}
		}
		

String Functions

Remember in part 1 where we discussed using utf8_bin as the encoding method for our database tables? Well this is how we ensure consistency through our script:

<?php

	/* 
	* String function settings 
	*/
		mb_internal_encoding('UTF-8');	// Tell PHP that we're using UTF-8 strings until the end of the script use mb_ for string functions...
		mb_http_output('UTF-8'); 		// Tell PHP that we'll be outputting UTF-8 to the browser 

mb_internal_encoding('UTF-8'); configures our PHP script to deal with multi-byte characters strings as UTF8, meaning we can safely manipulate strings in the database (as we've stored those string using UTF8_BIN...). We are required to use the mb_ functions to do this however. 

mb_http_output('UTF-8'); configures the output buffer of PHP to send data to the browser also UTF8 encoded. This is only part 3 of a 4 part series however (read part 1 of this article series for more information). When we come to make a HTML5 template, we'll also use <meta charset="utf-8" /> in the <head> as the final stage.

Click here to read more about mb_ functions in PHP.

PHP Version

Since we're going to be using functional features only found in PHP version 7+, the next step is to test whether or not the system is actually running the correct version. We can use PHP_MAJOR_VERSION for this.

	/* 
	* Check version 
	*/
		if (PHP_MAJOR_VERSION  < 7){
			echo '<p style="text-align:center;">This framework only supports PHP 7.1 ></p>';
		}

The DOC_ROOT constant

Our framework is going to be making a fair few requests to the file system (grabbing class files, including template elements etc). Instead of manually defining the document root in the application specific configuration, we can automatically grab it using:

	/*
	* set up directory root
	*/
		define('DOC_ROOT',dirname(__DIR__));

We'll make it a constant, as we don't want the value of DOC_ROOT changing after it's instantiation. The value of DOC_ROOT is set grabbing the directory of the parent directory (locally, specific to this php file). Since the file path is /engine/config.php, if we just used the magic __DIR__ constant, we'd get a DOC_ROOT including the /engine/ folder, therefor we use dirname() to get the current directory of the parent directory of the current file. Think of it like the ../ operator. 

If for example the config file was buried in /engine/config/config.php, we'd then have to use dirname(dirname(__DIR__)); to find the current directory of the parent's parent...

Click here to read more about Magic Constants

Application Specific Configuration include

Every application built with this framework will obviously need a set of definitions specific to that application. In our case, we've provided /engine/app_config.php for this purpose, so the next few lines check whether this file exists and includes it:

	/**
	* grab the application specific configuration eg. dbatase connection, doc root and definition list
	*/
		if (file_exists(DOC_ROOT.'/engine/app_config.php')) {
			require(DOC_ROOT.'/engine/app_config.php');
		}

We'll look at the creation of this file later in the article series. Note: Most of the defined() checks later in this /engine/config.php file are there in case they're not defined in /engine/app_config.php.

Error Reporting

In the first of the defined() checks, we test to see if this script should run in DEBUG mode, and if so, spit out all errors! Note the defined('DEBUG') check - this means we can define DEBUG as false in /engine/app_config.php for a live environment.

	/*
	* set up display warnings for debug, defaults to true
	* default to error reporting on, unless core_config debug definition overrides in /engine/plugin/core_config
	*/
		if (!defined('DEBUG')){
			define ('DEBUG', true);
		}
		
		if (DEBUG){
			error_reporting(E_ALL);
			ini_set('display_errors', '1');
		}

Default Constants

In much the same way as the DEBUG constant, here we can test for / set a few things we know we'll need in the HTML template later. The interesting one is the SESSION_IGNORES constant, which we'll reference further down the file. The SESSION_IGNORES constant stores a list of URL's that we may not want the session to have started in (for example when building an API).

Note: We're using the ALL CAPS format for constants for easy recognition in future files.

	/*
	* Default constants - override these in /engine/app_config.php
	*/
		if (!defined('SITE_NAME')){ define('SITE_NAME','Lectric'); }									//for ,eta title
		if (!defined('SITE_LINK')){ define('SITE_LINK',$_SERVER['SERVER_NAME']); } 						//url, defaults to nothing
		if (!defined('SITE_DESCRIPTION')){ define('SITE_DESCRIPTION','Lectric Default Installation'); }	//for meta desc
		if (!defined('DEFAULT_DIRECTORY')){ define('DEFAULT_DIRECTORY','default'); }					//for view directory selection
		if (!defined('SESSION_IGNORES')){ define('SESSION_IGNORES', []); }								//if scripts need to set own headers, they can be ignored for seesion start further down.
		if (!defined('VIEW_ON_FAILED_DO_REQUEST')){ define('VIEW_ON_FAILED_DO_REQUEST', true); }		//how do we deal with bad URL /do/ requests? View or not...

URL_NODES

The code to deal with the URL is extremely important:

	/*
	* define base URL NODES, URL_REQUEST AND REQUEST_METHOD
	*/
		//base request
		define ('URL_REQUEST', $_SERVER['REQUEST_URI']);
		define('URL_PATH', parse_url(URL_REQUEST, PHP_URL_PATH));
		define ('REQUEST_METHOD', $_SERVER['REQUEST_METHOD']);
		
		//re-register get params
		define('REQUEST_QUERY_STRING', parse_url(URL_REQUEST, PHP_URL_QUERY));
		mb_parse_str(REQUEST_QUERY_STRING, $_GET);
		//set up nodes
		$lecNodes = explode('/', trim(URL_PATH, '/')); //trim important for URL_NODES index numbers
		$lecNodes = array_filter( $lecNodes, function($value) { return $value !== ''; });
		define('URL_NODES', $lecNodes);

First of all, we define a few more contants that will get used throughout the execution, URL_REQUEST, URL_PATH and REQUEST_METHOD.

In the case of URL_PATH, we're using parse_url with our defined URL_REQUEST to return a string WITHOUT any aditional parameters (GET).

Then, in the inverse of this operation, we define REQUEST_QUERY_STRING as everything EXCEPT the URL_PATH, i.e. any GET params. This is only half the story however. Remember in the /.htaccess file that we forwarded the whole request to /index.php? Well this means that we need to rebuild the appended URL parameters back into the $_GET variable. We do this using mb_parse_str(), a function designed to take URL encoded strings and put them into a container.

Finally, the URL_PATH is split into an array, after being trimmed of '/' at the beginning and end of the string. As the comment indicates, the trim is important to ensure our URL_NODES array contains the correct number of nodes in the correct index. To make doubly sure that's the case, we also pass that array through a filter, getting rid of any blank values using array_filter(). The result of these operations is defined as URL_NODES.

Read more about parse_url() here.

Read more about mb_parse_string() here.

Autoloading the right way

PHP is an interpreted language, parsed and executed at run-time. As such, it's important not to bog the execution of any given script down by unneccessarily including files we don't need. A common mistake is to include ALL class files that MIGHT get used in any given pathway through the code, however there is a better way of handling this.

Welcome spl_autoload_register(). This function takes a function name is it's argument and uses that to tell PHP where to look whenever someone wants to include a class file. As PHP is object oriented (or at least, it can be) this has the effect of only "including" a class file when it's first needed. This saves memory and execution time.

The nice thing is that you can have any number of functions loaded into the autload queue by using multiple spl_autoload_register() calls. PHP will go through each function registered in order until it either finds the requested file, or doesn't.

	/*
	* define autoloader for lectric classes
	*/
		function lecAutoload($className): void 
		{
			
			$classnameBits = explode('\\', $className);
			
			//lectric Library
				if(count($classnameBits) === 2){
					if (file_exists(DOC_ROOT.'/library/'. $classnameBits[0] .'/'. $classnameBits[1] .'.class.php')){
						include_once(DOC_ROOT.'/library/'. $classnameBits[0] .'/'. $classnameBits[1] .'.class.php');
					}
				}
			return;
		}
		spl_autoload_register('lecAutoload');
		
	/**
	* grab the Composer vendor autloader if it exists
	*/
		if (file_exists(DOC_ROOT.'/vendor/autoload.php')) {
			require(DOC_ROOT.'/vendor/autoload.php');
		}

The lecAutoload() function takes in a $className argument, splits the string given to it by PHP into "bits", then looks in the /library/ folder we set up at the beginning of the project. Note the first use of our DOC_ROOT constant. We're limiting our autoload function to only allow classes defnined by a namespace, then a class name - however if you want to adhere more strictly to PSR-4, then by all means implement away here. In our case, we can structure our /library/ folder into /library/namespace/ folders, inside which we have className.class.php files. Check part 1 of this articles series to see that we set up a "Lectric" namespace folder already.

After registering our auto load function, we then give provision to include a Composer autoload file too, as it's likely any given application will want to include packages using Composer. This check assumes we''l put the /vendor/ folder for Composer in the document root of the project.

Database Connection and Handler through PDO

In our framework, we'll use PDO. Because you should. Namely, it allows easy prepared statements and a more secure way of talking to a database.

	/*
	* get the database connection from core_config, or set to null (if DB not needed)
	*/
		if (defined('DB_HOST') && defined('DB_NAME') && defined('DB_USER') && defined('DB_PASSWORD')){
			try { 
				$lecDBH = new \PDO('mysql:host='.DB_HOST.';dbname='.DB_NAME.';charset=utf8mb4', DB_USER, DB_PASSWORD);
				$lecDBH->setAttribute(\PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
				$lecDBH->setAttribute(\PDO::MYSQL_ATTR_USE_BUFFERED_QUERY, FALSE);
				$lecDBH->setAttribute(\PDO::ATTR_STRINGIFY_FETCHES, false);
				//return database data type instead of all strings
				$lecDBH->setAttribute(\PDO::ATTR_EMULATE_PREPARES, false);
			}  
			catch(PDOException $e) {  
				echo $e->getMessage(); exit;
			} 
		} else {
			$lecDBH = null;
		}

First we check if DB_HOST, DB_NAME, DB_USER and DB_PASSWORD have been defined (we would set these application specific constants in /engine/app_config.php), and if so then attempt to open a connection to that database, storing the result in a handler $lecDBH

This block of code is surrounded in a try /catch control block to catch if this connection has failed. Here we're assuming that if we can't get to the database, then we should stop execution of the application.

There are a few noteworthy options we can set in the database handler:

  • ATTR_ERRMODE - Ensure that if a query execution fails, that the handler throws an exception. This allows us to catch that exception in a database class we'll create later.
  • MYSQL_ATTR_USE_BUFFERED_QUERY - We want the data to remain with the handler (or specifically on the db server) whilst we do something with it. We'll kill out every request resource in the aforementioned database class.
  • ATTR_STRINGIFY_FETCHES - This makes sure we get back data in the same TYPE as it's stored in the database, otherwise integers on the database are returned as their string equivalents. Nope!
  • ATTR_EMULATE_PREPARES - Amongst other stuff, this also means that we can't bundle multiple queries into one statement. This can avoid confusion.

SESSION or not SESSION

Sometimes, we don't want PHP to start a SESSION, or more specifically, most of the time we want a SESSION to start, but occasionally that stops us from doing something. Usually involving not sending any headers.

	/* 
	* SESSION 
	*/
		if (( !in_array(trim(URL_REQUEST,'/'), SESSION_IGNORES) )){
			if (session_status() == PHP_SESSION_NONE) {
				session_start();
			}
		}

This code checks to see if the URL requested exists in our SESSION_IGNORE constant array. If it doesn't then as long as the session hasn't already started (PHP_SESSION_NONE) then go ahead and start it.

Conclusion

And that's the basic framework configuration file complete! In the next article in the series, we'll take a quick look at an example application specific configuration file, and then move onto our controller!

Continue to Part 3 by clicking here.




Hide Sidebar >



Archive



Search


Hide Sidebar >


This website uses cookies. Privacy Policy