Coding a PHP 7 Framework #3 - Code Workshop

In part 3 of Coding a PHP Framework, the application specific configuration is dealt with, followed by the routing controller for all requests.



Coding a PHP 7 Framework #3 - Code Workshop

1st September 2018 in   PHP Tutorials by Elliott Barratt

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 ->

Application Specific Configuration

The /engine/app_config.php file exists to house any application specific configuration and definitions. In part 2 of this article series, we constructed the main /engine/config.php file, which tested for the presence of a number of default constants. 

The framework will work without any application specific configuration (as we'll see when we get to the /library/Lectric/controller.class.php file) however, keeping a seperation of concerns is good practice.

An example /engine/app_config.php file is as follows:

<?php

	/*
	* Set DB connection details
	*/
		if (strpos($_SERVER['HTTP_HOST'],'dev.url') !== false) {
			// DEV
			define('DB_NAME', 'dev_db');
			define('DB_USER', 'dev_user');
			define('DB_PASSWORD', 'devpass');
			define('DB_HOST', 'localhost');
			
			/*Error Reporting on, turn off after deployment. */
			define('DEBUG', TRUE);
		} else {
			// LIVE
			define('DB_NAME', 'live_db');
			define('DB_USER', 'live_user');
			define('DB_PASSWORD', 'livepass');
			define('DB_HOST', 'localhost');
			
			/*Error Reporting on, turn off after deployment. */
			define('DEBUG', FALSE);
		}


	/*
	* Timezone 
	*/
		date_default_timezone_set('Europe/London');
		
	/* 
	* core definitions
	*/
		define('SESSION_IGNORES', []);
		define('DEFAULT_DIRECTORY', 'public');
		define('SITE_NAME','Demo Website');
		define('SITE_LINK','site.url');
		define('SITE_DESCRIPTION','Some description about the Demo Website');

In the above file, we test to see whether or not we should use Dev / Live database connection details by checking $_SERVER['HTTP_HOST'] for the existance of 'dev.url'. In the same test, we turn on / off error reporting with the DEBUG constant (first seen in part 2).

The core definitions go through the constants that are tested for existence in the /egnine/config.php files allowing us to override them. For this tutorial, we won't worry about SESSION_IGNORES. The important constant here is DEFAULT_DIRECTORY as that will be used in the upcoming controller class.

The Fat Controller

Up until this point, our framework hasn't really done anything except define a bunch of stuff and try to connect to a database. Well that ends here, with the /library/Lectric/controller.class.php file:

<?php
namespace Lectric;

/**
* Controller routing class for do/view
*
* The controller determines whether do and action or vieww something
*
* @package    Lectric Framework
* @author     Elliott Barratt
* @copyright  Elliott Barratt, all rights reserved.
*
*/ 
class controller {
	
    /**
     * construct contains the main do or view logic
     *
     * @param array URL_NODES array of URL nodes 
	 *
     */
		function __construct(&$DBH = null)
		{
			
			/*
			* define if view or not 
			*/
				define('VIEW', 
					(URL_PATH === '/') ?
						true : 
						((URL_NODES[0] !== 'do') ? 
							true : 
							((!isset(URL_NODES[1])) ?
								true :
								( (URL_NODES[1] !== 'response' && URL_NODES[1] !== 'action') ?
									true:
									false
								)
							)
						) 
				);
			
			if (VIEW === true){
				$lecView = new view($DBH);
			} else {
					
				try{
					switch(URL_NODES[1]){
						case 'response':
							/*
							* Response type do
							*/
							$lecDo = new doResponse($DBH);
						break;
						case 'action':
							/*
							* Action type do (no response, may generate another view or do something else...?)
							*/
							$lecDo = new doAction($DBH);
						break;
					}
				} catch (\Exception $e){
					
					if(DEBUG){
						\Lectric\controller::setSessionMessage($e->getMessage());
					}
					//graceful request error handling
					if(VIEW_ON_FAILED_DO_REQUEST === true){
						$lecView = new view($DBH);
					}
					
				}
				
			}
			
		}
	
    /**
     * Set session messages
     * 
     * @param string $msg 
     * 
     * @return void
     */
		public static function setSessionMessage(string $msg): void
		{
			
			if (!isset($_SESSION['lec_msg'])){
				$_SESSION['lec_msg'] = [];
			}
			
			if (trim($msg) !== ''){
				$_SESSION['lec_msg'][] = $msg;
			}
			
			return;
			
		}
	
    /**
     * Get session messages
     * 
     * @param string $msg 
     * 
     * @return void
     */
		public static function getSessionMessages(): ?array
		{
			
			if (isset($_SESSION['lec_msg'])){
				if (empty($_SESSION['lec_msg'])){
					return null;
				} else {
					return $_SESSION['lec_msg'];
				}
			} else {
				return null;
			}
			
		}
	
    /**
     * Unset Session messages if present
     * @return <type>
     */
		public static function clearSessionMessages(): void
		{
			
			if (isset($_SESSION['lec_msg'])){
				unset($_SESSION['lec_msg']);
			}
			return;
		}
	
}

Let's walk through this file in order:

Namespace

Although not strictly required for our framework to run, it's always good practice to parcel your classes up in a namespace. Don't know about namespaces? Read about them here. Briefly, they're containers we put classes in to prevent conflicts with class names. This will become very important when we start working with the /library/Lectric/view.class.php file. Also, notice that the namespace is the same as the library subfolder the class resides in (/library/Lectric/).

<?php
namespace Lectric;

Construct

The construct function is the main decision maker, and determines whether or not to serve up a view, a response (none view), or execute an action:

     /**
     * construct contains the main do or view logic
     *
     * @param array URL_NODES array of URL nodes 
	 *
     */
		function __construct(&$DBH = null)
		

When we instantiate this object in /engine/init.php, we pass the controller the database handle $lecDBH. Instead of making a new handle every time we instantiate an abject, we'll be very strict about passing that handle through to the child objects. This is VERY important, and makes our application all that more efficient. You'll find that nearly all classes we create extend a database class of some kind. Further to that, we're only using the $DBH variable by reference, prepending '&' to it in the construct function definition.

			/*
			* define if view or not 
			*/
				define('VIEW', 
					(URL_PATH === '/') ?
						true : 
						((URL_NODES[0] !== 'do') ? 
							true : 
							((!isset(URL_NODES[1])) ?
								true :
								( (URL_NODES[1] !== 'response' && URL_NODES[1] !== 'action') ?
									true:
									false
								)
							)
						) 
				);

Next we define the VIEW constant. This code is four nested short-form if statements, but translates to this:

  1. Is the URL_PATH constant set up in the /engine/config.php file '/' (i.e. website.com rather than website.com/something/)? If yes, then it's a view.
  2. If no, then is the first URL_NODES array value 'do'? If no, then it's a view.
  3. If yes, does the next node exist (URL_NODE[1])? If no, then it's a view (and presumably, we'll serve up a 404 error...)
  4. If yes, is URL_NODES[1] either 'response' or 'action'? If no, then it's a view.
  5. If yes, then it's not a view, so false.

Obviously, we cannot now have webpage URL's using /do/ as the first part, but knowing that we can build around it. However, this test will catch if people try to get to it, and better yet if the permissions on the /do/ folder in the directory structure are set correctly (711 or similar), then most servers will default to a 403 page anyway.

			if (VIEW === true){
				$lecView = new view($DBH);
			} else {
					
				try{
					switch(URL_NODES[1]){
						case 'response':
							/*
							* Response type do
							*/
							$lecDo = new doResponse($DBH);
						break;
						case 'action':
							/*
							* Action type do (no response, may generate another view or do something else...?)
							*/
							$lecDo = new doAction($DBH);
						break;
					}
				} catch (\Exception $e){
					
					if(DEBUG){
						\Lectric\controller::setSessionMessage($e->getMessage());
					}

					//graceful request error handling
					if(VIEW_ON_FAILED_DO_REQUEST === true){
						$lecView = new view($DBH);
					}
					
				}
				
			}

Assuming the given URL request is not a view, then we move on to determine whether or not it's a response or action that's being requested. We don't need to worry about a default clause on the switch code, as the test for anything else has already been made in defining the VIEW constant above.

The switch is encapsulated in a try/catch block. That's because in each of the doResponse and doAction classes there will be a number of checks about the URL request, and failing those will result in an \Exception being thrown. If an \Exception is thrown (and caught), depending on the value of VIEW_ON_FAILED_DO_REQUEST we instantiate a view class anyway to gracefully feedback to the user (in the case of an API, we may set VIEW_ON_FAILED_DO_REQUEST to false to prevent a REST service from spitting out html via the view class). Note the use of DEBUG to set a session message that only dev's could see...

The relevant objects are instantiated, again passing the referenced $DBH handle through.

Here are some examples of valid URL's:

  • http://somewebsite.com/category/webpage/ (Loading up a webpage from the database via the /library/Lectric/view.class.php object)
  • http://somewebsite.com/articles/category/blog-post/ (Loading up a webpage from the database via the /library/Lectric/view.class.php object, and this webpage loads up a blog post in a certain category)
  • http://somewebsite.com/do/response/folder/script/ (Getting a response from a particular file, maybe an API?)
  • http://somewebsite.com/do/action/namespace/class/function/ (Performing a do_ function. Maybe a POSTed form? (i.e. the "method" of the form...))

Invalid URL's might be:

  • http://somewebsite.com/do/ (This would result in a view)
  • http://somewebsite.com/do/something/ (This would result in a view)

SESSION messages

The following three functions are fairly simple, and provide a site-wide means of setting messages for user feedback. We can also use these messages in DEBUG mode to pass through Exception codes and messages should we want to redirect instead of stop execution.

You'll notice these functions utilise both argument type hints, and return type definitions. In the case of getSessionMessages(): ?array the ?array definition means we can return either an array OR null.

Click here to read more about PHP7 function features.

Conclusion

Now we're getting somewhere! Our framework can accept URL's and decide what to do based on the structure of that URL. In the next part of this article series, we'll create the doAction and doResponse class files.

Continue to Part 4 by clicking here.




Hide Sidebar >



Archive



Search


Hide Sidebar >


This website uses cookies. Privacy Policy