Spiria logo.

Tutorial: The file structure in a ProcessWire site

May 27, 2021.

Coming to grips with a programming universe always takes time. The available ProcessWire documentation, while excellent, doesn’t yield a complete picture of a site’s structure. As I have been asked to explain the philosophy and behaviour of the CMS many times, I thought I would write this introduction.

The reader is, of course, invited to browse the official documentation and to join the discussion groups. You can also check out the weekly newsletter. The great simplicity of the CMS/CMF conceals its power, which you can harness.

I don’t pretend to know everything that goes on under the hood when a page is loaded in ProcessWire. This article merely describes the process.

The structure of the files

The diagram below illustrates the basic structure of files and directories. This layout shows the path a page takes before it is displayed.


Two main directories: wire and site

The wire directory contains the ProcessWire code. It is, of course, immutable. When updated, though, this directory is automatically renamed .wire-version-number and the ensuing code is copied into a new wire. This is the basic principle for all updates in this CMS. So if you need to roll back, just delete the current version and rename the directory with its original name. (For modules, you just remove the period. For example, when updating the TracyDebugger module, the old code will now be in .TracyDebugger. ProcessWire will not attempt to read the code in this type of directory.) These archive directories should be excluded from the GIT.

One central index.php file sits at the root of the site. The existence of a composer.json file means that other PHP libraries can be grafted on using Composer and placed in the vendor directory. The programmer accesses these libraries through the various files described below.

The site’s programming all takes place in the site directory. We’ll come back to this later.

The database

The administration interface supports the creation of fields and page templates, as well as the installation of modules. Very little is written for ProcessWire in this database. Each field has its own table, which is mainly cache (that can be emptied), pages, modules, and templates.


Generally, you neither have to worry too much about the database nor about upgrading ProcessWire, because the CMS never alters the original structure. This should not prevent you from making backups before upgrading, which ProcessWire will prompt you to do. You can rest easy with ProcessWire. An update will never result in you having to rewrite code.

ProcessWire has many functions to gather information from a page (pages()->find(selectors) being an example). Going about it with direct SQL queries may seem like a daunting task, but a few modules, especially the excellent RockFinder3, make such calls manageable. The programmer will be faced with many database queries. By gathering all the elements of a page in the variable $page, ProcessWire creates a complex object in which all the transformations have been made. There are ways of bypassing these calls through caches and modules and by improving performance.

You cannot prepare a database ahead of time, as you would with the YII framework. The relationships between the tables are of a different nature than what you would expect from a traditional relational database. However, the more you work with ProcessWire, the more you realize that the concept of a “page” in a tree structure allows you to link data together (see the example below). The content itself becomes a relational database that can be manipulated within the site’s administration. To gain a fuller understanding of this, read the ProcessWire documentation .

General items

Even before a page appears, ProcessWire prepares many overall variables. These variables have methods, which the API describes in detail. They are always visible in the code and can be called in two ways. For example, the $pages class and its methods take into account all the pages of the site, which means that we can search for all the pages of any given model. The search selectors are varied and versatile:

$result = $pages->find(‘template=x’);


$result = pages()->find(‘template=x’);

This second way allows you to access all the components of the $pages object in an editor like PHPStorm. The displayed page is, of course, $page without an s (or page()).

Some other important general notes:

  1. cache(): the cache can be programmed, categorized in namespace, created, or deleted at will.
  2. config(): There are two config.php files, one in the wire directory and the other in site. All the configurations are gathered in this variable. Note the presence of config.env.php which is called up by config.php. This file is the local configuration, and is never stored in GIT. It is neither mandatory nor documented in ProcessWire. This is the way Spiria handles development sites when several of us work on the same project. We usually insert the coordinates of the local database and some debugging parameters here.
  3. session(): The equivalent of the $_SESSION variable on steroids. Additionally, ProcessWire enhances several PHP variables. This is the case for input() (which collects everything related to $_GET and $_POST).
  4. sanitizer(): This important class cleans up the input() entries.

Loading a page

Let’s assume we are browsing https://www.site.com/onePage. For now, let’s ignore any additional segments such as ?id=1234&u=abssd, which are handled by input() and URL segments. Let’s also set aside the preliminary reading of active modules.

ProcessWire has already recognized the (template) associated with the URL. In our example, we called it myModel. Before accessing this template, ProcessWire reads the init.php file. At this stage, the $page object is not complete. We can’t access the fields yet. We can place the first calls such as browser language detection or any other programming considerations in this file, but it is not mandatory. ProcessWire will only try to read it when it is there.

Next, ProcessWire checks for a class associated with the myModel template, in the classes directory. This is a recent addition to ProcessWire. If a DefaultPage.php file is present, functions included in this class will transfer to each page as if these methods were inherent to the template. So useful! (See explanation forcustom classes.)

class DefaultPage extends Page

For each template, we can extend the DefaultPage class :

class MyModelPage extends DefaultPage{

Or you can ignore it altogether:

class MyModelPage{

The purpose of these classes is to make the code more generic. The last example is perhaps unnecessary. Since the class is not related to the previous one, you might as well put the functions of these classes in the template itself, located in the templates directory.

But we can’t visit this directory. We have yet to encounter the ready.php file. As its name suggests, the page() object is now ready to be processed because we have access to the fields and methods. This lets us create hooks to the active modules, but also to the processes triggered when a page is displayed or edited.

We could, for example, intercept a page before it saves to make alterations.

Note in passing that it is possible to create one or more modules that will have a similar function to the init.php and ready.php files, because each newly created module will have an init() and ready() function.

Finally we get to the content of the templates to display our page! Our template myModel.php must be defined. This file is now ready to process the $page object. Examples:

<h1><?= page()->title ?><h1>
<div class="summary"><?= page()->title ?></div>
<div class="photoGallery"><?= page()->getGallery() ?></div>

The $user variable

Don’t forget this important variable. $user and its buddy $users include the current visitor’s information, and all registered users. If the user is anonymous (i.e. not logged in), she or he will be designated as a guest. By reading the page described above, we can easily detect the language used:

$lang = user()->language->name

An example

Let’s take an author’s website. The site includes descriptions of his or her published books, reviews, a page about him or her and a blog. The file structure would look like this:


The view directory is optional, as it depends on the code organisation strategy you have chosen. In a previous article, I described the strategies for rendering a page.

This structure is roughly that of my personal site. Here is a partial description of how the administrative hierarchy is built:


You will notice that some pages -- the reviews -- do not have a corresponding PHP file. This is the beauty of ProcessWire. Page templates can coexist without files because their fields are added to the database. This allows for all manner of relationship strategies, including the one from my site.

Each published book has gotten reviews. I have therefore created a template to collect them, which includes, in addition to the text, a field for the date, the author of the review, etc.

To display the book reviews, look for the page’s children. The variable $critiques contains all the reviews in the form of objects:

$critiques = page()->children();

This is how they could be displayed:

<?php foreach($critiques as $critique): ?>
  <div class="critique">
<div class="critiqueText"><?=$critique->body?></div>
<p class="critiqueAuthor">
<?=$critique->author?> • <?=$critique->date?>
<?php endforeach; ?>

In conclusion

This article is too short to understand ProcessWire in its entirety. The CMS designer has taken great care to deal with all aspects of a web publication, be it pagination, ajax calls, curl, mail, etc.

Once you understand the basic structure, the program gives you the freedom to build the simplest to the most complex sites.