project/
├── index.php
├── license.txt
├── readme.html
├── wp-activate.php
├── wp-admin/
├── wp-blog-header.php
├── wp-comments-post.php
├── wp-config-sample.php
├── wp-content/
│ ├── index.php
│ ├── plugins/
│ └── themes/
├── wp-cron.php
├── wp-includes/
├── wp-links-opml.php
├── wp-load.php
├── wp-login.php
├── wp-mail.php
├── wp-settings.php
├── wp-signup.php
├── wp-trackback.php
└── xmlrpc.php
project/
├── config/
│ ├── environments/
│ │ ├── development.php
│ │ ├── staging.php
│ │ └── production.php
│ └── application.php # Primary wp-config.php
├── vendor/ # Composer dependencies
└── web/ # Virtual host document root
├── app/ # WordPress content directory
│ ├── mu-plugins/
│ ├── plugins/
│ ├── themes/
│ └── uploads/
└── wp/ # WordPress core
wp plugin install redirection # Install a plugin and commit the code
wp plugin update redirection # Update a plugin
composer require 'wpackagist-plugin/redirection' # Install a plugin
composer outdated # Check for updates
composer update 'wpackagist-plugin/redirection' # Update a plugin
{
"name": "roots/bedrock",
"type": "project",
"license": "MIT",
"description": "WordPress boilerplate with modern development tools, easier configuration, and an improved folder structure",
"homepage": "https://roots.io/bedrock/",
"repositories": [
{
"type": "composer",
"url": "https://wpackagist.org",
"only": ["wpackagist-plugin/*", "wpackagist-theme/*"]
},
],
"require": {
"php": ">=7.2",
"composer/installers": "^1.8",
"vlucas/phpdotenv": "^4.1.8",
"oscarotero/env": "^2.1",
"roots/bedrock-autoloader": "^1.0",
"roots/wordpress": "^5.6",
"roots/wp-config": "1.0.0",
"roots/wp-password-bcrypt": "1.0.0",
"wpackagist-plugin/redirection":"^5.0",
"wpackagist-plugin/classic-editor": "^1.5",
},
"require-dev": {
"phpunit/phpunit": "^9.0",
"wpackagist-plugin/query-monitor": "^3.3"
}
}
NOPE. There is no built-in way to manage configuration for each environment.
Here is how we used to do it.
/**
* /wp-config.php
*/
if (file_exists(dirname(__FILE__) . '/wp-config-local.php'))
require_once(dirname(__FILE__) . '/wp-config-local.php');
/**
* /wp-config-local.php
*/
define('DB_NAME', 'wordpress');
define('DB_USER', 'wordpress');
define('DB_PASSWORD', 'wordpress');
define('DB_HOST', 'mariadb');
define('AUTH_KEY', 'fill-in');
define('SECURE_AUTH_KEY', 'fill-in');
define('LOGGED_IN_KEY', 'fill-in');
define('NONCE_KEY', 'fill-in');
...
define('WP_DEBUG', true);
define('WP_DEBUG_DISPLAY', true);
define('DISALLOW_FILE_MODS', true);
Bedrock replaces wp-config-local.php with separate environment variable and configuration methods.
The modern standard for storing environment variables is the .env file. Now we can have it in WordPress.
All thanks to Bedrock's implementation of the phpdotenv package.
# /.env
# PROJECT SETTINGS
PROJECT_NAME=example
PROJECT_TITLE="Example Project"
PROJECT_BASE_URL=example.localhost
# DATABASE CREDENTIALS
DB_NAME=wordpress
DB_USER=wordpress
DB_PASSWORD=wordpress
DB_HOST=mariadb
# Environment
WP_ENV=development
WP_HOME=http://${PROJECT_BASE_URL}
WP_SITEURL=${WP_HOME}/wp
# SECURITY HASHES
AUTH_KEY='000'
SECURE_AUTH_KEY='000'
LOGGED_IN_KEY='000'
...
# LICENSE KEYS
ACF_PRO_KEY='fill-in'
GRAVITY_FORMS_KEY='fill-in'
<?php
/**
* /config/environments/development.php
*/
use Roots\WPConfig\Config;
Config::define('WP_DEBUG', true);
Config::define('WP_DEBUG_DISPLAY', true);
Config::define('WP_DISABLE_FATAL_ERROR_HANDLER', true);
Config::define('SCRIPT_DEBUG', true);
ini_set('display_errors', '1');
Config::define('DISALLOW_FILE_MODS', false);
Timber adds Twig templating with an MVC structure to WordPress themes.
Let's start by creating a viewmodel!
<?php
namespace App\ViewModels;
class NewsArticleViewModel
{
public static function createFromPost($post)
{
$viewModel = new NewsArticleViewModel();
$viewModel->title = $post->title;
$date = new \DateTime($post->post_date);
$viewModel->publicationDate = $date->format('F d, Y');
$viewModel->introduction = get_field('introduction');
return $viewModel;
}
}
Now create the view using Twig.
{# pages/news-article.twig #}
{% extends "templates/base.twig" %}
{% block content %}
<div class="news-article-page">
<h1 class="news-article-page__title">
{{ post.title }}
</h1>
<div class="news-article-page__date">
{{ post.publicationDate }}
</div>
{% if post.introduction %}
<div class="news-article-page__introduction">
{{ post.introduction }}
</div>
{% endif %}
<div class="news-article-page__body">
{{ post.body }}
</div>
</div>
{% endblock %}
Finally, let's hook it all up with our controller.
<?php
/**
* single-news.php
*/
namespace App;
use App\Http\Controllers\Controller;
use Rareloop\Lumberjack\Http\Responses\TimberResponse;
use Rareloop\Lumberjack\Post;
use Timber\Timber;
class SingleNewsController extends Controller
{
public function handle()
{
$context = Timber::get_context();
$post = new Post();
$context['post'] = NewsArticleViewModel::createFromPost($post);
return new TimberResponse('patterns/pages/news-article.twig', $context);
}
}
Compare this to a traditional version of the same template where everything is in the controller.
/*
* single-news.php
*/
<div class="news-article-page">
<h1 class="news-article-page__title">
<?php the_title(); ?>
</h1>
<?php
$date = new /DateTime(the_date());
$publicationDate = $date->format('F d, Y');
?>
<div class="news-article-page__date">
<?php echo $publicationDate; ?>
</div>
<?php
if (get_field('introduction')) {
echo '<div class="news-article-page__introduction">' . get_field('introduction') . '</div>';
}
?>
<div class="news-article-page__body">
<?php echo get_field('body'); ?>
</div>
</div>
Now that we have pulled out the templates from WordPress template files we are free to organize our templates
in a way that supports our Atomic Design methodology.
/web/app/themes/custom/patterns
├── bits
│ └── button
│ ├── button.config.js
│ ├── button.styl
│ └── button.twig
├── components
│ └── image
│ ├── image.config.js
│ ├── image.styl
│ └── image.twig
│ └── text
│ ├── text.config.js
│ ├── text.styl
│ └── text.twig
├── pages
│ ├── landing
│ | ├── landing.config.js
│ │ ├── landing.styl
│ │ └── landing.twig
├── partials
│ └── news-card
│ ├── news-card.config.js
│ ├── news-card.styl
│ └── news-card.twig
└── templates
│ └── base
│ ├── base.config.js
│ ├── base.styl
│ └── base.twig

Fractal pattern library.
Lumberjack is a framework for your theme that allows you to "write better, more expressive and easier to maintain code."
It builds on the foundation of Bedrock and Timber.
Lumberjack supports...
// The way we used to do it.
// functions.php
function create_posttype() {
register_post_type( 'profile',
array(
'labels' => array(
'name' => __( 'Profile' ),
'singular_name' => __( 'Profile' )
),
'public' => true,
'has_archive' => true,
'rewrite' => array('slug' => 'profile'),
'show_in_rest' => true,
)
);
}
add_action( 'init', 'create_posttype' );
/* The Lumberjack way
app/PostTypes/Profile.php */
namespace App\PostTypes;
use Rareloop\Lumberjack\Post;
class Profile extends Post
{
public static function getPostType()
{
return 'Profile';
}
protected static function getPostTypeConfig()
{
return [
'labels' => [
'name' => __('Profile'),
'singular_name' => __('Profile'),
'add_new_item' => __('Add New Profile'),
],
'public' => true,
'rewrite' => array('slug' => 'profile'),
];
}
}
Now register the post type
/* Lumberjack
config/posttypes.php */
return [
'register' => [
App\PostTypes\Profile::class,
],
];
// How we used to do it.
/* Set the WP_Query arguments */
$args = array(
'post_type' => array( 'project' ),
'order' => 'ASC',
'orderby' => 'title',
);
/* The Query */
$query = new WP_Query( $args );
// The Lumberjack way
use App\PostTypes\Project;
$projects = Project::builder()->get();
dump($projects);
/*
Collection {
#items: array:2 [
0 => App\PostTypes\Project,
1 => App\PostTypes\Project,
]
}
*/
use App\PostTypes\Project;
$projects = Project::builder()
->orderBy('title', 'asc')
->limit(3)
->get();
use App\PostTypes\Project;
use App\PostTypes\CaseStudy;
use Rareloop\Lumberjack\QueryBuilder;
$posts = (new QueryBuilder)->wherePostType([
Project::getPostType(),
CaseStudy::getPostType(),
])
->orderBy('date', 'asc')
->get();
namespace App\PostTypes;
use Rareloop\Lumberjack\Post as LumberjackPost;
class Post extends LumberjackPost
{
...
public function scopeFeatured($query)
{
$featuredPostIds = [1, 2];
return $query->whereIdIn($featuredPostIds);
}
}
// page.php
// Get the latest 3 featured posts
$posts = Post::featured()
->orderBy('date', 'desc')
->limit(3)
->get();
<?php
return [
/**
* config/menus.php
* List of menus to register with WordPress during bootstrap
*/
'menus' => [
'main-nav' => __('Main Navigation'),
'footer' => __('Footer Navigation')
],
];
// routes.php
Router::get('path/to/page', 'TestController@show');
// app/Http/Controllers/TestController.php
namespace App\Http\Controllers;
class TestController
{
public function show()
{
return 'Hello World';
}
}


<?php
/* functions.php */
acf_add_local_field_group([
'key' => 'group_1',
'title' => 'My Group',
'fields' => array (
array (
'key' => 'field_summary',
'label' => 'Summary',
'name' => 'Summary',
'type' => 'textarea',
),
array (
'key' => 'field_categories',
'label' => 'Categories',
'name' => 'categories',
'type' => 'checkbox',
'width' => '33%',
'choices' => array (
array (
'faculty_staff' => 'Faculty & Staff',
'community_impact' => 'Community Impact',
'professional_development' => 'Professional Development',
)
)
)
),
'location' => array (
array (
array (
'param' => 'post_type',
'operator' => '==',
'value' => 'post',
),
),
),
'menu_order' => 0,
'position' => 'normal',
'style' => 'default',
'label_placement' => 'top',
'instruction_placement' => 'label',
]);

<?php
use App\ACF\FieldsBuilder;
use App\PostTypes\News;
$builder = new FieldsBuilder('news');
$builder->setLocation('post_type', '==', News::getPostType());
$builder->addRichText('summary')
->setLabel('Summary')
->setInstructions('A summary (about 25 words) of the article.')
->setRequired();
$builder->addCheckbox('categories')
->addChoice('Faculty & Staff')
->addChoice('Community Impact')
->addChoice('Professional Development')
->setWidth('33%')
->setInstructions('Select all categories that apply.');
return $builder;

Follow or connect with me on LinkedIn.
When functionality is added with more and more plugins and configuration screens it becomes impossible keep track of how things work.
- Read: Have you ever inherited a CMS project that was a mess of plugins and configuration. We can read and discover code much easier configuration in a UI. - Write: I remember the first time someone told me they didn't use the Views module in a Drupal project. - IDE: When you write modern code your IDE becomes invaluable. - DEV/TEST/PROD: Check out the 12 Factor App principles.
- It is a flat structure. - The web root is the project root.
* Web root is no longer the project root. Allows for code, config, and documentation that is not exposed to the web server * `wp-content/` is now `app/`
- WP-CLI is a powerful tool and allowed developers to manage plugins from the command line. - Bedrock takes it to the next level and allows dependency management that is standard for PHP using Composer. - All packages hosted on Wordpress.org have a WPackagist package. - Github only packages can be added as well.
We can and do use the .env file for more than just WordPress.
We are starting with just some basic PHP code to create our viewmodel object from the WP Post.
- Lumberjack is a theme-based framework. - The packages are all Composer dependencies.
You now have about a dozen chainable methods available to configure your query. Your IDE knows all about these methods and can autocomplete and hint at the parameters. Your IDE cannot help you figure your $args array.
Registering menus is not difficult in Wordpress. However, just have a single location to register them and abstracting away the Wordpress method into a simple array is so much nicer.
- ACF has support for making Gutenberg blocks using ACF fields that is more structured. - But there are other challenges with Gutenberg that keep us from adopting it. - We still prefer the ACF Flexible Content model.
Just one big specially formatted PHP object.
- Chainable methods. - Very powerful and verbose methods. - Sane defaults. - Extendable field types.