Alright, before I start showing the database stuff, I really want to talk about the architecture of this system. Sadly I've not been able to devote a lot of time into this the past week because, well... this is basically the equivalent of a normal client project, just without the paying clients. Guess who takes precedence.
Being Sunday and waiting for a client to get back to me which won't happen 'til tomorrow, I've got all day for this. Maybe I should mark Sunday as my day to work on this full time? A number of you seem enthused and interested, and it seems as good a hobby project as any to turn into a full blown CMS for the "normies".
Now, I do these types of file by file function by function breakdowns EVERY time I write a system like this, as not just a reference for myself and others, but as part of my debugging and refactoring process. It's amazing how often I'll find mistakes just by doing this.
The general architecture for this closely matches how PHP and HTTP requests work. HTTP is request driven, the request is our event. The HTML once CLIENT SIDE is what some programming models would consider the "view", where server side it's just "output". This is where IMHO MVC is a complexity mismatch as it tries to shoe-horn server side only many concerns that have no business on the server.
Hence if one were to make a cutesy acronym that sums up this programming model, it would be IPO.
Input > process > output
"Input" we figure out what the user wants or is trying to do, "process" we perform those actions and gather up any resultant data, then on "output" we glue those results to markup. Some "output" control -- session creation, gzip handling, and so forth -- ends up before input processing just as a safety precaution and so if we need to set things like headers or cookies during "input" or "process" we can.
When it comes to process, the overall concept is that we use the directory system to map user request handlers. Because the "one index to rule them all" is powered by this rewriteRule:
(which could be considered part of "input")RewriteRule !(^(images|downloads))|(\.(gif|jpg|png|css|js|html|txt|ico|zip|rar|pdf|xml|mp3|mp4|mpg|flv|swf|mkv|ogg|avi|woff|woff2|svg|eot|ttf|json|webmanifest)$) index.php
Only files in the two directories (images | downloads) and files with the above extensions are allowed to be served normally. ANY request that doesn't match either of those patterns is routed to our index.php where the requested path is parsed out of $_SERVER['REQUEST_URI']
With that in mind, let's just summarize files and each of their subsections.
/index.php -- the main user call.Note that what much of this file does is part of Squire, the file itself is not. It's just stuff I copy/paste in to wherever is appropriate for the turnkey solution.
// before we do ANYTHING else, let's set up gzip compression
foreach (['gzip', 'x-gzip', 'x-compress'] as $type) {
if (strpos($_SERVER['HTTP_ACCEPT_ENCODING'], $type) !== false) {
ob_start('ob_gzhandler');
ob_implicit_flush(0);
header('Content-Encoding: ' . $type);
register_shutdown_function(function() {
ob_end_flush();
});
break;
}
}
Blindly starting the gzip compression without manually setting the content-encoding can cause pages to load broken, same for trying to set this from php.ini or httpd.conf. By checking for all three possible compression types we actually can deliver gzipped to more UA's (IE, Konqueror, Blazer) than simply starting ob_gzhandler can handle, and it dodges the issue where Yandex will occasionally not index PHP pages that don't support x-compress as a parameter.
It's just better to manually start it and explicitly ob_end_flush with a shutdown handler.
Have you ever noticed that if you ob_start the zip handler, then try to DIE or PHP outputs an error, the gzip compression ends up mangled and you get a gibberish page? That's what registering a shutdown function to end the buffer accomplishes.
... and by starting output buffering as our first thing, we can header() or set cookies until blue in the face regardless of when/where in the code; even after we have output!
session_start();
/*
regenerating the ID on every pageload reduces the time window
in which a session key can be exploited for MITM attacks.
*/
session_regenerate_id();
I'd hope you folks would know what that does by now. I imagine the comment helps.
/*
prevent displaying site in frame. Some people will say do this
in the httpd.conf or other server config, it takes NO processing
difference here, it's easier to say do it only for our single
entry-way from here, I prefer not to put something this important
outside our portable code, and it's better not to make assumptions
about what server software is in use. Not everyone uses Apache.
*/
header('X-Frame-Options: DENY');
I know a LOT of dev's will say "don't do that from the php" -- why? PHP is going to run anyways, it can set headers, it takes no more processing time --
in fact it can take LESS because we don't need a regex to not blanket apply it from the config files -- and putting it in our single entry point is just simplest.
I see a lot of people get their panties in a knot over using header() for anything but redirects which is funny, I'm the exact opposite.
I hate redirects and avoid them whenever possible./*
Load functions and methods common to all pages
*/
include('libs/common.lib.php');
Speaking of bad commenting practices, did I REALLY have to explain that?
/*
Many settings are loaded from ini as the setter and are
only retrievable with a getter.
*/
Settings::loadFromIni(
'default.ini.php',
'user.ini.php'
);
The Settings static object comes from common.lib.php. It can load settings from ini files or directly set them. Values from ini files cannot be directly overwritten, though when it becomes Paladin that will change... slightly. Paladin uses a more complex flavor of this same routine. For now just know that these ini files values are being loaded.
define(
'TEMPLATE_PATH',
'templates/' . (Settings::get('template') ?: 'default') . '/'
);
There's an old saying,
use define... a lot. No, A LOT!The template path is a perfect candidate for this. We will need it all over the codebase, it can be handy inside the template itself since our URI's could be almost anything, so let's set it. Once we get to Paladin this value will likely be changed to TEMPLATE_DEFAULT_PATH since the user may have their own value. Multi-skinning -- like most forums offer -- being an essential part of our structure.
templateLoad('common');
Loads TEMPLATE_PATH . 'common.lib.pgp' using require_once. See common.lib.php for more. It's just easier to have a function for a method that we call not just here, but inside actions as well. Some actions might need the forms.template.php, others might have their own.template.php.
Note that template_load actually checks for if the file exists in TEMPLATE_PATH, but then also an optional
$action_path parameter. This allows actions to contain their own default template separate from the system one, letting actions be modular. When making separate templates everything falls back on the default or the action. We'll talk about that more in the common.lib.php discussion.
/*
IIFE to isolate scope, so a code appendage can't see anything
we want to keep local here just in case. Thankfully PHP now has
proper IIFE
*/
(function() {
Comment says it all. This will become main() in the proper "paladin" system.
$action = Request::value();
if (!$action) $action = 'static';
if (!is_dir('actions/' . $action)) httpError(404);
$actionPath = 'actions/' . $action . '/' . $action . '.';
safeInclude($actionPath . 'startup.php');
$data = action_startup();
template_header($data);
if (file_exists($fn = $data['contentFilePath'] . '.content.php')) {
safeInclude($fn);
action_content($data);
}
if (file_exists($fn = $data['contentFilePath'] . '.static')) readfile($fn);
template_footer($data);
We set our $action to be included, reverting to "static" as the default if none is found. We 404 if there's no corresponding action (see httpError in common.lib.php), otherwise we include the startup file.
Startup files are our "process". A normal process handler will return $data to be output (if any) including the "contentFilePath", which can be a PHP file that performs output logic, or a static file, OR BOTH! Note that statics are always loaded after PHP driven content.
The template is auto-added as appropriate. Note that some PROCESS may call the template themselves and then die() for the handling of special cases such as errors.
In a full Paladin system I often condense down httpError and so forth to methods on a "Bomb" static that also calls error_log.Alright, gimme a few and I'll do the same for common.lib.php