PHPainfree2 Docs

Program Structure - Documentation | PHPainfree2

PHPainfree2 Program Structure

A detailed overview of the PHPainfree2 framework structure.

By default, PHPainfree2 is designed to provide just enough structure to allow you to rapidly build complex PHP-powered web sites and applications.

Default Structure

The default project structure focuses on three main top-level directories:

  • htdocs/ - publicly accessible files and program entry-point.
  • includes/ - Application objects, code, and Controllers.
  • templates/ - Application views, templates, and partials.
PHPainfree/
|-- htdocs/
|   |-- .htaccess
|   |-- index.php
|   |-- css/
|   |-- js/
|   `-- images/
|-- includes/
|   |-- PainfreeConfig.php
|   |-- Painfree.php
|   |-- App.php
|   `-- Controllers/
`-- templates/
    |-- app.php
    `-- views/

PHPainfree2 Request Flow

Before diving in to PHP and looking at specific files, it's important to understand the flow of a request into a PHPainfree2 application.

1. Initial Request - htdocs/.htaccess

When a request comes in to your Apache server running a PHPainfree2 application, the request is first processed by .htaccess (1). This Apache .htaccess file will ignore any URL that is trying to retrieve a file or directory that exists, but for all other URLs will grab the entire request path and rewrite the URL to index.php?route=$1.

Thus, a request to server.com/some/path/to/load will be transformed into index.php?route=/some/path/to/load, adding the entire path into a $_REQUEST['route'] query parameter and passing off the entire request to index.php (2).

htdocs/.htaccess

RewriteEngine On

RewriteBase /

RewriteCond %{SCRIPT_FILENAME} !-f
RewriteCond %{SCRIPT_FILENAME} !-d
RewriteRule ^(.+)$ index.php?route=$1&%{QUERY_STRING} [L]
				
PHPainfree/
|-- htdocs/
|   |-- .htaccess
|   |-- index.php
|   |-- css/
|   |-- js/
|   `-- images/
|-- includes/
|   |-- PainfreeConfig.php
|   |-- Painfree.php
|   |-- App.php
|   `-- Controllers/
`-- templates/
    |-- app.php
    `-- views/

2. First Script - index.php

Once the request has been processed and passed off to htdocs/index.php, PHPainfree2 takes over.

htdocs/index.php

<?php
set_include_path(get_include_path() . PATH_SEPARATOR . '../');

// Uncomment if you're using Composer for PHP modules
// require realpath('../vendor/autoload.php');

include 'includes/Painfree.php';
				
PHPainfree/
|-- htdocs/
|   |-- .htaccess
|   |-- index.php
|   |-- css/
|   |-- js/
|   `-- images/
|-- includes/
`-- template/

3. PHPainfree2 Framework - includes/Painfree.php

Painfree.php does a tiny bit of setup, and first loads includes/PainfreeConfig.php (4) before creating an instance of class PHPainfree.

Painfree.php Sequencing

  1. Start an execution timer for benchmarking [ $__painfree_start_time ].
  2. require 'PainfreeConfig.php'; to bring in your application configuration settings.
  3. Create an instance of class PHPainfree to process the rest of the request and pass application control to the Application Singleton. $Painfree = new PHPainfree($PainfreeConfig);
  4. Automatically load ANY .php files located in the includes/Autoload folder.
  5. Load the ApplicationController script defined in your PainfreeConfig.php file. include $Painfree->logic();
  6. Lastly, after all application logic has been executed, load the BaseView template defined in your PainfreeConfig.php file. include $Painfree->view();
includes/Painfree.php

$__painfree_start_time = microtime(true);

require 'PainfreeConfig.php'; // you must have this file

$Painfree = new PHPainfree($PainfreeConfig);
$Painfree->URI = $_SERVER['SERVER_PORT'] == 80 ? 'http://' : 'https://';
$Painfree->URI .= $_SERVER['HTTP_HOST'] . $_SERVER['REQUEST_URI'];

// process Autoload folder
$loaders = $Painfree->autoload();
foreach ( $loaders as $load ) {
	include $load;
}

include $Painfree->logic(); // load the application logic controller 
include $Painfree->view();  // load the view

class PHPainfree {
	// cut for brevity
				
PHPainfree/
|-- htdocs/
|-- includes/
|   |-- PainfreeConfig.php
|   |-- Painfree.php
|   |-- App.php
|   `-- Controllers/
`-- templates/

4. Configuration - includes/PainfreeConfig.php

PainfreeConfig.php, read in by Painfree.php and passed directly into the new PHPainfree($PainfreeConfig); constructor, does a lot of the heavy lifting in application setup.

This file contains a single array named $PainfreeConfig that defines some of the default routes, connects to the database, and specifies specific folders and scripts necessary for PHPainfree2.

Suggestions

When you first download PHPainfree2, the default configuration provided loads a copy of this application and documentation to serve as an example of how to structure a project. However, this framework was designed to be non-opinionated, meaning that you are intended to use this framework in a manner that best suits your design intentions.

ApplicationController, BaseView, and DefaultRoute should be considered a starting-point to launch your development efforts. In most projects, you'll most-likely want to rename the ApplicationController from App.php to YourProjectName.php as well as changing the name of the class.

The class App {} singleton instance that PHPainfree2 will create handles all of the routing and business logic for your program, so it's helpful to pick a short name for this class and the instance it will create that matches your goals for your project. This will be covered in more detail in step 5, below.

PHPainfree/
|-- htdocs/
|-- includes/
|   |-- PainfreeConfig.php
|   |-- Painfree.php
|   |-- App.php
|   `-- Controllers/
`-- templates/
includes/PainfreeConfig.php

<?php
	$PainfreeConfig = array(
		'ApplicationController' => 'App.php',
		'BaseView'     => 'app.php',
		'DefaultRoute' => 'main',
		'Database'     => array(
			'PrimaryDB' => array(
				'host'   => $_ENV['MYSQL_HOST'],
				'user'   => $_ENV['MYSQL_USER'],
				'pass'   => $_ENV['MYSQL_PASSWORD'],
				'schema' => $_ENV['MYSQL_SCHEMA'],
				'port'   => $_ENV['MYSQL_PORT'],
			),
		),
		'RouteParameter' => 'route',
		'TemplateFolder' => 'templates',
		'LogicFolder'    => 'includes',
	);
				

5. ApplicationController: includes/App.php

Now comes the fun part!

This file is the heart of your specific application or website. At this point, PHPainfree2 is almost completely finished with all the magic that it does and you're expected to do the rest of the application development yourself.

The provided App.php makes an excellent starting point for your project. This script, and the singleton you're expected to create, will serve as the overall application state of your program for each and every HTTP request that is passed to the server and handled by PHPainfree2.

For more detailed information about the ApplicationController, please see /docs/painfree-application-controller.

PHPainfree/
|-- htdocs/
|-- includes/
|   |-- PainfreeConfig.php
|   |-- Painfree.php
|   |-- App.php
|   `-- Controllers/
`-- templates/

class App Overview

The provided ApplicationController is a stripped down version of several production ApplicationControllers that have been developed for several large companies serving hundreds of thousands of requests for complicated highly-interactive data-driven web applications. This file is an excellent starting point for projects of all sizes, ranging from quick Hackathon projects to high-availability production web applications.

Environment Setup - includes/App.php

At the top of the ApplicationController, the code creates a global Singleton instance of class App defined in this file.

Reminder

This ApplicationController is just code that is automatically executed by $Painfree->logic() and DOES NOT have to be fully defined in this file. Some products are better suited having the ApplicationController class stored in a different file and defined with proper namespacing, or perhaps loaded automatically through a PSR-4 composer autoload mechanism.

Build things to suit YOUR needs, not the framework's needs.

Eric Harrison, PHPainfree Structure Documentation

Further Exploration

The ApplicationController is explored in detail in /docs/painfree-application-controller, but we'll quickly touch on the "magic" that happens here as a part of the request.

includes/App.php

<?php
	// It's common to rename the "App" class and object instance to match
	// your specific product. Feel free to leave it as $App or rename it.
	$App = new App();
				
includes/App.php

		class App {
		// snipped...

		public function __construct() {
			global $Painfree;

			$this->BASE_PATH = str_replace('/htdocs', '', $_SERVER['DOCUMENT_ROOT']);

			// $this->db = $Painfree->db;
      // OR
			// require_once 'core/MySQLiHelpers.php';
			// $this->db = new MySQLiHelpers($Painfree->db);
			
			// Set up the route and prepare for routing
			$this->route = $Painfree->route;
		}
				

"Magic" Routing

Now that the route variables have been prepared, the route() method does the next four pieces of magic in a few short lines of code.

  1. Check if there is a controller script defined for $App->view.
    • If Controller Exists, load and execute the controller script.
    • If No Controller, send a HTTP 404 status code.
  2. htmx Support [partials] - Check the Request Headers to see if HX-Request is set and set a boolean for our templates to use.
  3. htmx Support [boosted links] - Check the Request Headers to see if HX-Boosted is set and set a boolean for our templates to use.
  4. Fast API Support - Check the request headers to see if Content-Type is set to "application/json" or a query parameter of ?json was sent with the URL, and if so, set the response Content-Type and immediately die and send back the contents of $this->data encoded as JSON.

End of $App->route() : void;

In the case of an API JSON request, execution will immediately halt and output the contents of $App->data as a JSON blob. In all other cases, execution returns to $Painfree to render and load the templates.

In this ApplicationController, we leave execution having performed these important tasks:

  1. Processed the URL into $App->view, $App->id, and $App->action.
  2. Executed any application-specific controller code found at includes/Controllers/{$App->view}.php.
  3. Set any special boolean application state variables for htmx in $App->htmx and $App->htmx_boosted.
includes/App.php

			header('X-Frame-Options: SAMEORIGIN');

			if (file_exists("{$this->BASE_PATH}/includes/Controllers/{$this->view}.php")) {
				require_once "Controllers/{$this->view}.php";

				$headers = apache_request_headers();
				if ( isset($headers['HX-Request']) && 
					$headers['HX-Request'] === 'true' ) {
					$this->htmx = true;
				}
				if ( isset($headers['HX-Boosted']) && 
					$headers['HX-Boosted'] === 'true' 
				) {
					$this->htmx_boosted = true;
				}
				if ( isset($headers['Content-Type']) && 
					$headers['Content-Type'] === 'application/json' ) {
					header('Content-Type: application/json; charset=utf-8');
					die(json_encode($this->data));
				}
			} else {
				header('HTTP/1.0 404 Not Found');
				// We don't "die" here so that the developer can render a
				// nice looking 404 template if they want.
			}
				

Rendering a Template BaseView

After completing the route() method, execution returns to $Painfree and $Painfree->view(); is called. This function loads the script defined in your PainfreeConfig.php BaseView option, which is the script located in templates/. In the PainfreeConfig provided with this project, the initial BaseView template is defined as templates/app.php.

Your initial BaseView template is often going to be the most complicated template in your project, performing double-duty as an HTML skeleton for the rest of your views as well as dynamically loading view templates based on the URL. In PHPainfree2, this template also has code to handle htmx partial templates.

Dynamic View Template Loading

Inside of templates/app.php in the meat of our web template, we'll load a template file in the template/views/ directory if one exists with the same name as the value stored in $App->view.

In PainfreeConfig, we have our DefaultRoute parameter defined as "main", so our application will serve templates/views/main.php for any request to either http://hostname.com/ (no path) as well as explictly called like http://hostname.com/main. The value of that view, "main" automatically loads:

  • includes/Controllers/main.php
  • templates/views/main.php

This is one of the designs that makes developing projects with PHPainfree2 so quick. Adding new pages is as simple as dropping a file in the includes/Controllers/ folder and templates/views/ folder. And as you develop more complicated applications, allowing all of your Controller code to serve as the business logic for your REST JSON API, you're able to do a lot more with a lot less duplication.

templates/app.php

	<body id="app-body" class="bg-dark text-light">
<?php
		include 'header.php';	
?>

<?php
	if ( file_exists("{$App->BASE_PATH}/templates/views/{$App->view}.php") ) {
		include "views/{$App->view}.php";
	} else {
		include "views/404.php";
	}
?>

<?php
		include 'footer.php';	
?>

PHPainfree/
|-- htdocs/
|-- includes/
`-- templates/
    |-- app.php
	`-- views/
	    `-- main.php
templates/app.php

<?php
if ( 
	$App->htmx && 
	! $App->htmx_boosted && 
	file_exists("{$App->BASE_PATH}/templates/views/{$App->view}.php") 
	) {
	// If we are an htmx request and the "view" variable exists in the top-level
	// templates folder, render that as an HTMX snippet.
	//
	// If we are an htmx request and there is a "sub-view" defined that lives
	// inside a folder, render _THAT_ instead of the full top-level snippet.
	//
	// In _this_ application, we're overriding $App->id to act as our default
	// "sub-view" route, but you should feel free to write whatever type of 
	// routing architecture that you want.
	//
	// This example requires that a top-level /templates/views/{$view}.php file 
	// exists **AND** a top-level /templates/views/{$view}/{$id}.php file to
	// exist for this magic to occur. 
	//
	// Each application built with PHPainfree should design their routing and
	// template relationships however best suits that product.
	$file_path = "{$App->BASE_PATH}/templates/views/{$App->view}/{$App->id}.php";
	if (file_exists($file_path)) {
		include_once "views/{$App->view}/{$App->id}.php";
	} else {
		include_once "views/{$App->view}.php";
	}
} else { 
?>
<!DOCTYPE html>
<html lang="en">
	<head>
		<title><?= $Painfree->safe($App->title()); ?></title>

		<link rel="icon" type="image/x-icon" href="/images/favicon.ico" />