A highly polymorphic architecture
Towards the end of 2019, I developed a desire to father a few online content outlets. I am a pretty prolific dude - always working on a handful of (what I deem to be, at least) exciting schemes, designs and projects. As a self-professed INTJ and Arts graduate, it is simply in my nature. I find it immensely fulfilling creating things, and will be abundantly motivated to do this regardless of any external factors. However, keeping all of my work private, regardless of its public worth or relative inconsequentiality, created a sense of inefficiency and incompleteness.
I decided then, to create two websites to document some of my output: socket-one.com for right-brained things, and socket-two.com for left-brained things.
Socket one would feature more traditionally creative work such as illustrations, videos and recorded music, and also things that are irreverent, experimental or unfinished. Socket-two.com would primarily feature writing and programming, and work that is generally more refined and austere.
At the time that I was considering this, I also maintained a website that promoted my services as a freelance Web Developer, which consisted largely of a portfolio gallery. I decided that this could also benefit from a (re)implementation.
Fleshing out the requirements
My basic requirement was therefore in the shape of three small, serialised-content-driven websites. What was interesting about this requirement - aside from the obvious symmetry in technical scope - was the small overlap in purpose: While most records would quite clearly belong to one site or another, it was foreseeable that every so often, a particular project would logically belong to several.
(A note to any search engine optimisation enthusiasts that may be cringing at this notion: Yes, featuring the same material in a different location can be slightly bad for Google page rankings, but Google page rankings are only of absolute importance if you are running a content farm. *shivers*).
While I wanted to be able to vend like content in multiple locations, I wanted to avoid having content that was duplicated in the sense of duplicated data; existing as several identical files or database records.
I also wanted to avoid the tedious and unnecessary task of creating three separate systems, when each did virtually the same thing. I decided that it would also be undesirable to have three pasted copies of the same server side code floating around on the server filesystem.
My solution was a little bit unconventional, which is why I thought it might be interesting enough to share.
In other words, the solution would strive for a complete elimination of duplicated code - and content sources - of any kind, among a cluster of similar websites.
The worry that the efficiency of so much shared resource would be trumped by an insufficient freedom to diverge among web applications caused me to double check my design. However, I concluded that the similarity of purpose, and a resolve for careful, judicious separation of base entities from app-level extensions validated the architecture.
Having previously found browser-based WYSIWYG editors to be less convenient than conventional text editors, I also had a desire for a system that leveraged files as the vehicle for content-to-database shuttling. I also wanted to be able to choose between writing the textual portion of a piece of content in either markdown format or HTML, as dictated by the nature of each publication.
I would also seek to keep the final solution as lean as possible, avoiding the use of any unnecessary build tools, package managers, or third party frameworks. Like any other developer in his or her right mind, I do encourage the use of build tools, package managers and third party frameworks, where such tools are appropriate. However, there is a recent trend for using them gratuitously, in situations whereby they do not bring a benefit. I will always advocate a use of such things that is characteristically Spartan - but not severe.
Macroscopic solution architecture
The diagram below is rudimentary, but it should loosely illustrate how the architectural aspects all fit together. As the websites I want to host are small, and each should not need to process a great deal of traffic, the whole solution is designed to exist on a single server.
Every aspect of the diagram represents a resource that is shared, except for the area in the bottom right - which depicts individual websites. These are independent of one another. Each website node leverages a large quantity of shared resource, but may, in addition, house it's own stylesheets, template images, idiosyncratic web components, and serverside classes.
The website nodes shown in the bottom right portion of the diagram all have an
index.php entry point, which is represented in the diagram by
EP). A central feature of the architectural concept is that many domain names will feed into the same application. An Apache webserver config file featuring
VirtualHost entries is responsible for setting up the domain-name to entry point associations, and the code below demonstrates exactly how this is accomplished. (The
DocumentRoot has been falsified for security reasons, but is still illustrative).
The last thing to cover is the content authoring process. This is represented by the top third of the diagram. As represented by the bubble on the top right, blocks of website content (articles, images, apps, and so on) exist as directories, and each directory houses textual content (in md or HTML format), content metadata (title, date of creation, a list of websites that the content should appear on), and media files.
To publish content to the website, the content building process, a PHP script, is run from the command line. This iterates through all directories within the content directory, copies media files to the media server, and inserts textual content appearing in HTML or md files, alongside metadata, into a database. Once the content is within the database and the media server, any website that is part of the cluster may access it (provided the metadata of the content in question permits it), via the serverside framework.
I should mention that I am writing in generic terms when I refer to a "media server". In the context of this solution, the "media server" is actually just a directory on the main server which has it's own subdomain. However, the solution could be reconfigured to use a media server such as Amazon S3. All images encountered by the media building process will be copied to this location at their original resolution, but also as smaller thumbnail versions for use in content lists.
A more detailed depiction of the solution architecture
The diagram below shows the whole solution, a bit more lucidly, divided into four parts. The
common code powers each of the
apps, which display the decentralised
build code implements a variety of automated tasks, including the establishing of the database structure, the seeding of the database with test records, the compilation of frontend assets, and the publishing of content by means of the processes described above. All build processes - including PHP based build processes - are grouped together and executable by means of an all-encompassing NodeJS script.
It's worth clarifying that the
Media box and
App box entities within the
Apps quadrant are not fully-fledged apps - they do not leverage the custom severside MVC framework. Instead, they are both just directories that hold files which can be served directly to the browser. The app box, which has not yet been explained, houses independent applications for demonstration purposes. Once established in this location, each demo application can be referenced by content records of the
app type, and subsequently linked to or embedded.
The text on the right depicts the (collapsed) file structure of the whole solution. This is shown to convey how the conceptualisation of the solution as
The custom server side framework
In consideration of a small website sat upon a LAMP stack, a simple, bespoke PHP MVC framework can be the perfect fit. It is common for a developer to habitually reach for a feature-rich third party solution, when it is not the optimal solution. In the case of this project, the list of wants was small, comprising an MVC foundation and a simple MySQL access layer. This, and the need to be able to elegantly join the said features onto some slightly unconventional code sharing structures, qualified a custom framework approach as the most apposite option.
I elected to use a minimalistic PHP MVC framework that I had created a few years prior, in 2014, as the basis for the new, bespoke foundations. However, reading through the code, I was dissatisfied with it, so I carried out a complete rewrite. The jocular namesake from the first iteration persisted though - the "Doge framework".
Designed for use in the context of small websites, and intended to speed up development time, the Doge framework was built around another fairly novel architectural concept: No routes, and optional controllers.
In order to create a page in a web app that leverages the Doge framework, all that is required is a view file. When the user hits the application entry point, the framework examines the request method and the URL. It then attempts to infer a view file path, and a controller name and method based on these variables. If a view file exists at the deduced file path, the HTML contents of the view in question is wrapped within the HTML of the template view file, and transmitted to the browser. If a corresponding controller method also exists, the controller method is called prior to rendering the view (and this is often done as a means of assembling computed data for use in the latter).
This mechanism demands that there are direct correlations between controller names, URLs, and view names. Placing such a demand on naming schemes could be frustrating in some projects (particularly larger ones), but in the context of microsites, it can be a helpful convenience.
The following diagram depicts the new, derived PHP framework structure. It is quite minimalistic, with the App Core file (actually called
Doge in the source code) and the
RootController (which all other application-specific controllers extend from) taking responsibility for all the route handling and related functionality that is described above.
Input entities are simply utility classes.
Input provides a facade for easily accessing and filtering data present in PHP's
SiteConfig is an object representation of the configuration file that appears within the root directory of the operative website. It features methods for correctly getting values from this data structure.
Laravel provides a wonderful API, but can be overkill for a small project. However, thanks to the work of rappasoft, its possible to download all of Laravel's excellent helper functions in the form of one portable file. This is fantastically convenient for a simple project such as this, so they were included. An additional helpers file completes the picture.
As one might expect, the boot file simply loads all of these scripts.
Order of events
The following list describes the process that delivers a user with content, by means of the Doge framework.
- A user opens their browser, and enters www.socket-two.com in their address bar.
- The user's browser is pointed to the socket two server, as per DNS implementation.
- The request is handed from TCP/IP to Apache.
- Apache's configuration file compels it to redirect all requests that are based on the domain name
index.phpfile loads more scripts, including the framework boot script, which loads scripts of its own. It then creates an instance of the core application class,
Doge, to which it passes global config data, and config data specific to the website that is being accessed.
- The instance of the
Dogeframework then sets to work, calling a controller method if applicable. This will typically be to fetch a resource record from the database, and promote the said data for rendering within the view.
- Finally, the framework sends a view file, which it interpolates with the site template, and any model data that might have been retrieved. If the request method and URL do not match any corresponding view file or controller method, a 404 response is rendered instead.
The file below shows the entry point for socket-two.com that is hit by Apache. All website faces that are part of the cluster have their own version of this file.The public entry point for the socket-two.com website
Above: Note how the configuration data for the operative site is passed into the Doge framework object. It is this configuration data that tells the Doge framework instance which universe it is living in, and which website is being requested.The Doge framework boot file
The architecture of an extending website
Having described the common libraries, the architecture of an extending website is fairly unremarkable, and is as one might expect. Regardless, the diagram below depicts this structure for the sake of completeness. Note that the common
Doge framework does not have an extending class for each website. Instead, all websites are implemented by an instance of the
Doge base class, and divergences in behaviour are achieved by the following mechanisms:
- A distinct configuration file for each website.
- A distinct server side entry point (index.php).
- Distinct serverside controllers and views.
- A distinct client side entry point (app.js), into which varying component sets are injected.
- A distinct SCSS entry point (app.scss), into which varying, idiosyncratic stylesheet sets are injected.
Naturally, an extending website may also load it's own idiosyncratic helper classes when this is desired.
Webpack is used to load web components from both common areas and areas that house components specific to the operative website. The
componentManager singleton provides a method
mountAllComponents to mount all components that are supplied to it by means of a component-listing object.
The following snippet demonstrates the boilerplate code that is used to describe a single component. Note that it loads the
Component prototype, and a few other things, from common areas.
As mentioned above, each website has it's own SCSS entry point. This simply loads the style scripts for all common mixins, variables, and declarations, the required common components, and then finally, idiosyncratic components.Socket two SCSS entry point file
The final piece of the puzzle is a deployment solution. I feel that it is important to be able to deploy a website easily. For a large project practising continuous delivery, it is crucial for keeping customers happy. For me, it helps ensure that a potentially time-consuming process does not make me reluctant to issue new content.
The deployment solution I use is a bespoke NodeJS command line utility, which I designed to supersede my own deployment bash scripts. The ability to represent websites, servers, and disks as objects is a key advantage to implementing a deployment solution in a high-level language. This lends itself well to a much more elegant implementation.
Resting upon this notion, the program can carry out deployments, or bidirectional backups of volumes, websites, and operating systems, in response to a simple command that features a source identifier and a destination identifier.
Having realised the cluster of websites in the intimately intertwined architecture described above, I can happily, honestly, and smugly report that the project was a great success.
- Circumventing the creation of three systems by creating a single, all-encompassing system did, in practice, reduce the workflow by nearly two thirds.
- Being able to fix or modify more complex components, such as the comments component, in one place, and have the fix instantly propagate was particularly handy.
- Having these applications encapsulated within a single system is logical and tidy, considering the similarity of each system's final cause.
- Having a content-authoring workflow that is common to several locations is a boon, and nullifies the cerebral overhead that swapping between a variety of systems could incur.
No side effects occurred as a result of the intense code-reusage patterns.
The success of this rather incestuous system is highly attributable to it's small size and commonality in website purpose. Using a very similar system in the context of a collection of more disparate websites would be unwise.