Create a Simple CMS with Backpack - Part 2

Continuing on from the first part, in this part we will actually build the simple cms addon.

in Backpack, Laravel, PHP

Lets get right into it: in the previous part we installed all the tools we need to progress, so lets create our package:

artisan package:new --i My Cms
# follow the steps
cd packages/My/Cms
git init
git add .
git commit -m "Package Skeleton for the CMS"

Ok, now lets get to building: --- well maybe a little explanation no the concept we are following:

most traditional system would go ahead an put together something like this for the database to say cover dealing with both news articles and event listings:

Field Type

News Article

Event

varchar

Headline

Event

varchar / foreignId

Author

Contact

Text

Teaser

Description

Text

Article

Event Details

Date

Publish Date

Start Date

Date

Archive Date

End Date

Ok great - we are done: lets build a model and off we go....

Uhmmm no... what happens when we need more fields for one or the other, like a location for the event, a category for the article and so on...

The traditional approach to this challenge is to create user-defined fields. A commercial CMS database I recently worked with had one large content table that had several extra fields of each data type: date1, date2, text_1, and so on. This solution does allow for future growth but has many serious issues, including:

  •  What do you do when you need more than two date fields?

  •  How will the next developer who takes over the project know what the text_2 field is in different contexts?

One solution to this challenge is the node pattern. This consists of two components: pages that are the main container and content nodes that are the actual blocks of content. A given page can have any number of content nodes.

The first step is to decide exactly what information you want to make available in the page object. This data is then broken down into items that are going to be required for every page and items that may be variable. The items that are required by every page are managed by the page component; these include the name and content type. Then the headline, main content, and any other content blocks are managed by the content node component.

Creating the content_nodes Table

The nodes table will be very simple because it will store the content as a list of key-value pairs, with one additional field to make the relationship to the page:

  •  id: The primary key

  •  page_id: The foreign key that is used to relate the content node to the page

  •  node: The key for the content node, such as headline or teaser

  •  content: The actual content data, can be of many types,

Lets create our migration, first create the following folders in the Packages/My/Cms folder:

  • database/factories

  • database/migrations

  • database/seeders

then run:

sail artisan make:migration cms_content_node --path=packages/My/Cms/database/migrations

and in the file

<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

class CmsContentNode extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
         Schema::create('cms_content_nodes', function (Blueprint $table) {
             $table->id();
             $table->unsignedBigInteger('page_id')->nullable()->default(null);
             $table->string('node')->nullable()->default(null);
             $table->json('content');
         });
    }

    /**
     * Reverse the migrations.
     *
     * @return void
     */
    public function down()
    {
        Schema::dropIfExists('cms_content_nodes');
    }
}

then your model at packages/src/Models/CmsContentNode.php

<?php

namespace My\Cms\Models;

use Illuminate\Database\Eloquent\Model;

class CmsContentNode extends Model
{

    protected $casts = [
        'content' => 'json'
    ];


    public function page()
    {
        return $this->belongsTo(CmsPage::class);
    }
}

Creating the pages table

The pages table will need to store information that every page must have:

  •  id: This is the primary key.

  •  type: This is the type of page, such as a news article.

  •  parent_id: This is the parent page.

  •  title : This is the name of the page.

  • slug: page slug

  •  created_at: This is the date/time the page was created.

  • updated_at: this is the date/time the page was updated

  • deleted_at: this is the date/time the page was deleted

so lets create our migration & model

sail artisan make:migration cms_pages --path=packages/My/Cms/database/migrations

<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

class CmsPages extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        Schema::create('cms_pages', function (Blueprint $table) {
            $table->id();
            $table->string('type')->nullable();
            $table->unsignedBigInteger('parent_id')->nullable()->default(null);
            $table->string('title');
            $table->string('slug')->unique();
            $table->timestamps();
            $table->softDeletes();
        });
    }

    /**
     * Reverse the migrations.
     *
     * @return void
     */
    public function down()
    {
        Schema::dropIfExists('cms_pages');
    }
}
 # AND THE MODEL
<?php

namespace My\Cms\Models;

use Backpack\CRUD\app\Models\Traits\CrudTrait;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\SoftDeletes;

class CmsPage extends Model
{
    use SoftDeletes;
    use CrudTrait;

    public function nodes()
    {
        return $this->hasMany(CmsContentNode::class, 'page_id');
    }
}

# then run
sail artisan migrate

Next lets setup the boilerplate for our controller, request and admin route:

# Controller : packages/My/Cms/Http/Controllers/Admin/CmsPageCrudController.php
<?php

namespace My\Cms\Http\Controllers\Admin;

use Backpack\CRUD\app\Http\Controllers\CrudController;
use Backpack\CRUD\app\Library\CrudPanel\CrudPanelFacade as CRUD;
use My\Cms\Http\Requests\CmsPageRequest;
use My\Cms\Models\CmsPage;

/**
 * Class PageCrudController
 * @package App\Http\Controllers\Admin
 * @property-read \Backpack\CRUD\app\Library\CrudPanel\CrudPanel $crud
 */
class CmsPageCrudController extends CrudController
{
    use \Backpack\CRUD\app\Http\Controllers\Operations\ListOperation;
    use \Backpack\CRUD\app\Http\Controllers\Operations\CreateOperation;
    use \Backpack\CRUD\app\Http\Controllers\Operations\UpdateOperation;
    use \Backpack\CRUD\app\Http\Controllers\Operations\DeleteOperation;
    use \Backpack\CRUD\app\Http\Controllers\Operations\ShowOperation;

    /**
     * Configure the CrudPanel object. Apply settings to all operations.
     *
     * @return void
     */
    public function setup()
    {
        CRUD::setModel(CmsPage::class);
        CRUD::setRoute(config('backpack.base.route_prefix') . '/cms-pages');
        CRUD::setEntityNameStrings('page', 'pages');
    }

    /**
     * Define what happens when the List operation is loaded.
     *
     * @see  https://backpackforlaravel.com/docs/crud-operation-list-entries
     * @return void
     */
    protected function setupListOperation()
    {
        CRUD::setFromDb(); // columns

        /**
         * Columns can be defined using the fluent syntax or array syntax:
         * - CRUD::column('price')->type('number');
         * - CRUD::addColumn(['name' => 'price', 'type' => 'number']);
         */
    }

    /**
     * Define what happens when the Create operation is loaded.
     *
     * @see https://backpackforlaravel.com/docs/crud-operation-create
     * @return void
     */
    protected function setupCreateOperation()
    {
        CRUD::setValidation(CmsPageRequest::class);

        CRUD::setFromDb(); // fields

        /**
         * Fields can be defined using the fluent syntax or array syntax:
         * - CRUD::field('price')->type('number');
         * - CRUD::addField(['name' => 'price', 'type' => 'number']));
         */
    }

    /**
     * Define what happens when the Update operation is loaded.
     *
     * @see https://backpackforlaravel.com/docs/crud-operation-update
     * @return void
     */
    protected function setupUpdateOperation()
    {
        $this->setupCreateOperation();
    }
}

# packages\My\Cms\Http\Requests\CmsPageRequest.php
<?php

namespace My\Cms\Http\Requests;

use Illuminate\Foundation\Http\FormRequest;

class CmsPageRequest extends FormRequest
{
    /**
     * Determine if the user is authorized to make this request.
     *
     * @return bool
     */
    public function authorize()
    {
        // only allow updates if the user is logged in
        return backpack_auth()->check();
    }

    /**
     * Get the validation rules that apply to the request.
     *
     * @return array
     */
    public function rules()
    {
        return [
            // 'name' => 'required|min:5|max:255'
        ];
    }

    /**
     * Get the validation attributes that apply to the request.
     *
     * @return array
     */
    public function attributes()
    {
        return [
            //
        ];
    }

    /**
     * Get the validation messages that apply to the request.
     *
     * @return array
     */
    public function messages()
    {
        return [
            //
        ];
    }
}

# packages/My/cms/src/routes.php
<?php

Route::group([
    'namespace'  => 'My\Cms\Http\Controllers\Admin',
    'prefix'     => config('backpack.base.route_prefix', 'admin'),
    'middleware' => array_merge(
        (array) config('backpack.base.web_middleware', 'web'),
        (array) config('backpack.base.middleware_key', 'admin')
    ),
], function () {
    Route::crud('cms-pages', 'CmsPageCrudController');
});

# edit packages/My/cms/src/CmsServiceProvider.php
<?php

namespace My\Cms;

use Illuminate\Support\ServiceProvider;

class CmsServiceProvider extends ServiceProvider
{
    /**
     * Perform post-registration booting of services.
     *
     * @return void
     */
    public function boot(): void
    {
        $this->loadMigrationsFrom(__DIR__ . '/../database/migrations');
        $this->loadRoutesFrom(__DIR__.'/routes.php');
    }

    /**
     * Register any package services.
     *
     * @return void
     */
    public function register(): void
    {

    }

    /**
     * Get the services provided by the provider.
     *
     * @return array
     */
    public function provides()
    {
        return ['cms'];
    }

}

## then run
sail migrate

Visit http://localhost/admin/cms-pages and lo and behold the pages list is there :-)

Next: our first basic pages, forms etc. - Next time