Statamic v2 ... On Laravel!

Written By Jesse Schutt
Posted on

I’ve been a fan of Statamic, and the guys behind it, since its inception in 2012. When I first learned about it, I found myself needing a simple way for clients to edit their content, while at the same time not wanting to deal with databases. I needed something I could turn around quickly.

Statamic has continued to mature, and when I learned that v2 was going to be rewritten on Laravel 5.1 I knew I had to dig a bit deeper, especially into what developing for the new codebase would be like. 

Before we get started, I want you to know this article is specifically about developing custom addons for Statamic and is not about building a site using it. Also, the Todo addon we will be discussing is available here. That said, onward!

Moving to Laravel has given us (and addon developers) access to a extensive list of features right out of the box. It’s also allowed us to write cleaner and more maintainable code. Adding new features can now be done in a fraction of the amount of time compared with v1, and with much higher confidence thanks to testability.

Jason Varga

Gentleman @ Statamic

Statamic has carved its niche by ditching the traditional database and utilizing a flat-file datastore instead. This has made editing content very simple, especially for folks that prefer writing markdown in a text editor. If that’s not your style, you may prefer to use the slick, responsive control panel.

Since all content is stored in files and there’s no database to reconcile, deploying and versioning content is quite simple.

One of the things I dislike most about setting up a traditional CMS is the sheer number of clicks it takes to configure an installation. All of Statamic’s core functionality can be configured via .yaml files, drastically reducing the need for navigating through a control panel. I like that!

Surveying the Statamic Landscape

My time at Zaengle is primarily focused on custom Laravel development, so I thought building a demo addon for Statamic v2 would be a good way for me to see how easy it is to implement traditional Laravel into a standalone CMS.

I learned a number of things and adjusted my expectations accordingly. It seemed the type of addon I decided to build turned out to be a little ahead of their Addon API rollout, but there are many things I’m excited about as the platform matures. Jack does a good job summarizing the anticipation I felt as I began reflecting on the Statamic/Laravel combination:

Statamic's future is vibrant, like the sun on a bright June morning. Wide reaching, like the wings of an albatross soaring high above the patterned quilt of the country side. Exciting, like the anticipation of the steep climb of a rollercoaster.

Jack McDade

Gentleman @ Statamic

I decided to build a simple Todo addon and I wanted to cover the following items:

  • Custom Control Panel Section
  • Dashboard Widget
  • Template

There are other sections that could be covered, such as Fieldtypes, Filters, or Tasks, but for this addon I focused on the list above. Let's get going!

Scaffolding and Boilerplate

Statamic ships with a helpful command line tool called Please that will, among other things, spit out a typical addon scaffolding. If you are familiar with Artisan, which is included with Laravel, then Please will be second nature.

Once I generated the scaffold, I started tinkering around with the boilerplate code provided. A lot of it looked very similar to what I'm familiar with in Laravel... which makes sense, because it is Laravel!

Requests to the addon are handled via the routes.yaml file, however the syntax is different than the regular Laravel routes file. Take a look at what I ended up with:

routes:
  /:
    as: todo
    uses: index

  getAll:
    as: todo.getAll
    uses: getTodos

  put@complete:
    as: todo.complete
    uses: markCompleted

  post@add:
    as: todo.postAdd
    uses: postAdd

  delete@delete:
    as: todo.delete
    uses: deleteTodo

As you would expect, these routes map to controller functions. So calling route('todo.getAll') in either your views or classes will fire the getTodos() method on TodoController.php.

Once I got to the TodoController things felt very much like home to me. There are a number of Statamic-specific methods available since the controller extends the base Addon class, such as $this->storage->getYAML('key'). The standard Storage, File, and Folder facades are available, but the ones that extend from the Statamic API will contextualize the storage path to the current addon. That means typing fewer characters, which is good!

TodoRepository

Since I knew that I would be dealing with the Todo data in several locations, I created a repository class that would centralize the CRUD related to the individual Todos. 

public function saveTodo($title)
{
    $timestamp = Carbon::now()->timestamp;

    Storage::putYAML('addons/Todo/' . $timestamp, [
        'title'     => $title,
        'completed' => false
    ]);

    // $this->prepTodo simply formats the todo data for the view
    return $this->prepTodo('addons/Todo/' . $timestamp . '.yaml');
}

public function markCompleted($todoId)
{
    $file = Storage::getYAML('addons/Todo/' . $todoId . '.yaml');
    $file['completed'] = true;
    $file['completedAt'] = Carbon::now()->timestamp;
    Storage::putYAML('addons/Todo/' . $todoId . '.yaml', $file);
}

public function deleteTodo($todoId)
{
    return File::disk('storage')->delete('addons/Todo/' . $todoId . '.yaml');
}

// more methods are in the github repository

In a typical Laravel application we would be using Eloquent and a database. It took me awhile to realize that I had to do things differently in the Statamic ecosystem. In essence I had the freedom to build my data structures however I saw fit. I chose to use individual .yaml files for each Todo, each containing values such as whether it was completed, when it was completed, and the title text.

There are other methods I could have used to organize the addon's data. For example, I could have created a single Todos.yaml file with an array of all the Todo data. Or I could have used a single Todos.yaml file that held an array of the file names of the individual Todos. 

The Statamic guys said they're working documentation showing several options for organizing and storing data. They also hinted that database support will be added in the future.

Optionally DB-backed Users and Collections are both among the things on our roadmap. A hundred-thousand user files in a folder is a bad idea, so these solutions are designed for higher-trafficked sites.

Jack McDade

Gentleman @ Statamic

Once the TodoRepository was established, the TodoController was populated with methods to handle requests from the frontend, and collect, parse, and return the appropriate Todo data. This all held closely to standard Laravel convention.

Control Panel Javascript

Statamic relies on the popular Vue.js framework to display data via components. By creating my own Todo Vue component in the site/addons/Todo/assets/js/scripts.js file, Statamic knew to autoload it and make it available in my views.

Vue.component('todos', {

    props: ['get', 'complete', 'add', 'delete'],

    data: function() {
        return {
            newTodo: {
                title: null
            },
            allTodos: {},
            ajax: {
                get: this.get,
                complete: this.complete,
                add: this.add,
                delete: this.delete
            }
        }
    },

    ready: function () {
        this.getOpenTodos();
    },

    methods: {
        getOpenTodos: function() {
            this.$http.get(this.ajax.get, function(data){
                this.allTodos = data.todos;
            }).error(function() {
                alert('There was a problem loading the Todos');
            })
        },
        
        addTodo: function() {
            this.$http.post(this.ajax.add, this.newTodo, function(data) {
                this.newTodo.title = null;
                this.allTodos.push(data);
                
            }).error(function(data) {
                alert('There was a problem adding the Todo');
            });
        },

        completeTodo: function(todo) {
            this.$http.put(this.ajax.complete, {id: todo.id}, function(data) {
                todo.completed = true;
                todo.completedAt = 'Just Now';
            }).error(function() {
                alert('There was a problem completing the Todo');
            });
        },
        trashTodo: function(todo) {
            this.$http.delete(this.ajax.delete, {id: todo.id}, function(data){
                this.allTodos.$remove(todo);
            }).error(function() {
                alert('There was a problem deleting the Todo');
            });
        }
    },
    computed: {
        hasOpenTodos: function() {
            var $totalOpen = 0;
            _.forEach(this.allTodos, function(value, key) {
                if(value.completed === false) {
                    $totalOpen++;
                }
            });

            return $totalOpen > 0;
        },
        hasCompletedTodos: function() {
            var $totalCompleted = 0;
            _.forEach(this.allTodos, function(value, key) {
                if(value.completed === true) {
                    $totalCompleted++;
                }
            });

            return $totalCompleted > 0;
        }
    }
});
Demo Final

Template Tag

In the event that a user would like to display the open Todos in a frontend view template, I created a TodoTags.php class which grabs all the relevant Todos from the TodoRepository and runs them through the $this->parseLoop() method. 

public function showOpen()
{
    $todoRepository = new TodoRepository();
    
    $todos = $todoRepository->getOpenTodos();

    return $this->parseLoop(
        $todos->map(function($todo){
            return [
                'todo_title' => $todo['title'],
                'todo_added' => $todo['added']
            ];
        })
    );
}

Todos can then be displayed in frontend templates like this:

<ul>
    {{ todo:show_open }}
        <li>{{ todo_title }} - {{ todo_added }}</li>
    {{ /todo:show_open }}
</ul>

Dashboard Widget

Adding data to the Statamic control panel dashboard is as easy as creating a TodoWidget.php class and returning a view from an html() method.

<?php

namespace Statamic\Addons\Todo;

use Statamic\Extend\Widget;

class TodoWidget extends Widget
{
    private $todoRepository;

    public function init()
    {
        $this->todoRepository = new TodoRepository();
    }
    public function html()
    {
        return $this->view('widget', [
            'openTodos' => $this->todoRepository->getOpenTodos($this->getConfig('limit', 5)),
            'addonTitle' => $this->getConfig('title')
        ]);
    }
}
Widgets Wide

So What Have I Learned?

Having spent a few days on the development of the Todo addon, a handful of observations have risen to the surface: 

1. Using Laravel in Custom Addons is Easy(ish)

As I was originally learning PHP (via CodeIgniter), I frequently found myself struggling to decipher what was framework code and what was vanilla PHP. I had a similar experience with the development of the Todo addon. 

At times it was obvious that I was using Statamic's api methods, but there were also times I needed to use base Laravel as well. This is to be expected, and is likely my issue more than Statamic's, but it took me longer than I'd hoped to differentiate between the two.

Jack and Jason were terrific in helping me understand the borders between Laravel and Statamic as well as thoughtfully adding api methods that improved what I was trying to accomplish.

2. Managing Addon Data is Flexible... if not Slightly Nebulous

Another challenge I didn't expect was how to organize the Todo data. Statamic offers several $this->storage() methods to interact with .yaml files, however I found myself wanting to revert back to the Eloquent calls I use in a typical Laravel application.

The Statamic team has indicated they will be adding examples to the official documentation illustrating how to interact with the file-based datastore. 

The fact is that developers have a lot of freedom in how we structure our addon-specific data. In the case of the Todo addon I chose to store todos in their own files, but could have easily stored all of them in a single todo.yaml file as an array.

This flexibility is powerful, but the lack of convention means developers have to figure out ways that work for our individual situations. 

3. The Statamic Community Rocks

I'm a huge believer in the importance of community, and the Statamic community is top-notch

I spent a good amount of time picking the brains of Jack, Jason, and Gareth while at a conference last week and I was impressed not only by the care these guys put into Statamic, but moreso by the respect and dignity they carry into the community. They are stand-up guys and it shows.

Take a stroll over to the Statamic Lodge, or jump into the Statamic Slack channel to get a taste of what I'm referring to!

Statamic's community has always been extremely friendly and happy to help each other out. Jack, Jason, and I all feel a sense of pride and purpose in helping the developers, new and old, in the friendliest manner possible all while cultivating a sense of kindness and camaraderie.

Gareth Redfern

Gentleman @ Statamic

As I mentioned at the beginning of this article, the thoughts I've presented relate directly to using Laravel within the Statamic ecosystem. Statamic's relationship with Laravel is still young, and I'm sure as time goes by we'll see consistent patterns emerge.

I'm excited about the possibilities. Very cool stuff is ahead!

  1. Cowboy Header
  2. Roller Coaster Image
  3. Factory Image