Silver Comet Logo

Updating a Laravel model to support publishing

Written 6th June 2023


Building a schedule tool

Overview

To build a scheduling tool we need to address a few items.

  • Our database table of choice needs to have a “publish_at” column for the model.
  • We need to update the model to handle the new column.
  • An Observer file needs creating.
  • We’ll update the EventProvider to accept the Observer.

Dependencies

This will work with most CMS’ of choice. In our example we’re going to use Backpack but really you could do this with your own build.

Why?

A scheduling tool is a must have if you’re not planning to be at the computer at your chosen scheduling time, or you need to schedule posts around times you’re working.

Database

Artisan

For our database tool we’re going to use Laravel’s built in migration tool.

php artisan make:migration add_published_at_column_to_blog_posts

Let’s take a closer look at this. php artisan make:migration is the command here - very self explanatory. We’ve provided the argument here of add_published_at_column_to_blog_posts which means artisan pre-populates some of the migration for us. In this example blog_posts is our table name.

Migration file

Let’s open our migration, you can find this under /database/migrations It’s going to be the one with the most recent datetime. Your file should look something like this. Note how our naming convention pre-populated the table for us.

<?php

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

return new class extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        Schema::table('blog_posts', function (Blueprint $table) {
            //
        });
    }

    /**
     * Reverse the migrations.
     *
     * @return void
     */
    public function down()
    {
        Schema::table('blog_posts', function (Blueprint $table) {
            //
        });
    }
};

We’re interested in the up() and down() methods here.

up()

We want to add our column here and define the column type.

public function up()
    {
        Schema::table('blog_posts', function (Blueprint $table) {
            $table->timestamp('published_at');
        });
    }

I’m using published_at as my column name as it follows the same naming convention as Laravel’s built in created_at and updated_at. You could argue that published_at shouldn’t be nullable, but remember we’re mimicking the created_at and updated_at.

down()

In case something goes wrong or you want to roll back for any other reason, you should always fill out the down() method. I regularly see this left blank.

public function down()
    {
        Schema::table('blog_posts', function (Blueprint $table) {
            $table->dropColumn('published_at');
        });
    }

Here we’re simply deleting (MySQL calls it dropping) the column we created.

Running the migration

Now in our terminal we simply need to run:

php artisan migrate

and we’ll populate the column in to our table.

Model

In our example we need to update our BlogPost model, this can be found at /app/Models/BlogPost.php.

Constant

We’re going to add a PUBLISHED_AT constant so we’re mimicking the CREATED_AT constant. This is probably unnecessary but as a developer preference I like to ensure my code can be extended in the same way as the item it’s adding to as much as possible.

/**
 * The name of the "published at" column.
 *
 * @var string|null
 */
const PUBLISHED_AT = 'published_at';

Casting

Next we should add our column as a cast so we can access it more easily within our code.

 protected $casts = ['published_at' => 'date'];

Fillable

Our fillable array should include our new column as well.

protected $fillable = [..., 'published_at'];

Observer

Now we’re creating something called an observer in our app, this essentially allows us to move events outside of our model so it doesn’t become cluttered.

Creating

We go in to our terminal and type:

php artisan make:observer BlogPostObserver --model=BlogPost

Populating the created() event

When the BlogPost is created, we’re going to sync our published_at column with our created_at column. This might seem needless, but it reduces the checks we need to do later on when accessing the posts, and to boot it makes it easier to view in the database.

/**
 * Handle the BlogPost "created" event.
 *
 * @param  \\App\\Models\\BlogPost  $blogPost
 * @return void
 */
public function created(BlogPost $blogPost)
{
    if (is_null($blogPost->published_at)) {
        $blogPost->published_at = $blogPost->created_at;
        $blogPost->save();
    }
}

Registering our Observer

Next we need to tell the event provider that we’re expecting it to pay attention to this file. Open app/providers/EventServiceProvider and add our Observer to the boot() method as below:

use App\\Models\\BlogPost;
use App\\Observers\\BlogPostObserver;
 
/**
 * Register any events for your application.
 */
public function boot(): void
{
		parent::boot();
    BlogPost::observe(BlogPostObserver::class);
}

Midway point

We’ve now created ourselves a new Migration and Observer, plus we’ve updated our Model and Event Provider. This will work for almost anyone and so you’re welcome to jump off here, but what if we wanted to be a little more fancy? There’s 2 major drawbacks with this application…

Drawback 1

We are relying on a user loading a page to change the content on the site, this is great until you want to start doing more with your chosen Model. Say we wanted to post to our social media platforms at the designated time? We might not want to give someone access to our social media directly but if our site pushed to social media instead, we want the article to be published first so we populate the OG schema.

Drawback 2

We’re relying on dates here. Developers hate dates. Not the romantic type you understand, they’re mostly fine, but the data type. They’re convoluted and even if you’ve got it down to a fine art, the person before or after you probably didn’t.

Going further

So let’s address those 2 drawbacks. Here’s what we’re going to do now:

  • Add a boolean status column.
  • Add a new method to our controller.
  • Create a command.
  • Schedule the command via the Kernal.

Status column

We’re going to create a new column, so we’re repeating the migration step here in reality. As a result I’m going to do this step in a little more shorthand than before.

php artisan make:migration add_status_column_to_blog_posts_table

Now edit the file this creates:

<?php

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

return new class extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        Schema::table('blog_posts', function (Blueprint $table) {
            $table->boolean('status');
        });
    }

    /**
     * Reverse the migrations.
     *
     * @return void
     */
    public function down()
    {
        Schema::table('blog_posts', function (Blueprint $table) {
            $table->dropColumn('status');
        });
    }
};

Now we edit our BlogPosts model again and add the status column under the fillable array.

protected $fillable = [..., 'published_at', 'status']

We also for the sake of ease want to add a constant to define our published state, this will come in handy when we’re looking at controllers.

public const STATUS_UNPUBLISHED = 0;
public const STATUS_PUBLISHED = 1;

Controller

In your controller file, you need to create a new method:

public function publish()
{
    $count = 0;
    $articles = BlogPost::where('status', BlogPost::STATUS_UNPUBLISHED)
                ->whereDate('published_at', '<=', Carbon::now())
                ->get();

    foreach ($articles as $article) {
        $article->status = BlogPost::STATUS_PUBLISHED;
        $article->save();
        $count++;
    }
    return $count;
}

You’ll need to add Carbon as part of the use statement block use Carbon\\Carbon;. We now have a controller method that does a foreach over each of our BlogPost articles that have dates set in the past - I’d suggest putting this in to jobs/batches but I’m not expecting loads of entries to be processed at once.

Command

Now we create our command, this allows us to publish via terminal.

php artisan make:command PublishBlogPosts

Open the file /app/Console/Commands/PublishBlogPosts.php and update it to look like this:

<?php

namespace App\\Console\\Commands;

use Illuminate\\Console\\Command;
use App\\Http\\Controllers\\BlogController;

class PublishBlogPosts extends Command
{
    /**
     * The name and signature of the console command.
     *
     * @var string
     */
    protected $signature = 'publish:blogposts';

    /**
     * The console command description.
     *
     * @var string
     */
    protected $description = 'Publish any blog posts thaat are due';

    /**
     * Execute the console command.
     *
     * @return int
     */
    public function handle()
    {
        $blogController = new BlogController();
        $count = $blogController->publish();
        $this->info($count . ' post(s) published');

        return Command::SUCCESS;
    }
}

Here we’re adding our controller to the use statement, then we’re calling our publish method. $this->info() means we’re reporting back our result to the terminal…speaking of which lets test the command runs by typing:

php artisan publish:blogposts

You should get a response such as 0 post(s) published.

Scheduling

So finally, let’s add our new command to the Kernal so it’s scheduled. Open app/Console/Kernel.php and edit the schedule method to have our new command, it should look something like this when you’re done:

protected function schedule(Schedule $schedule)
{
    $schedule->command('publish:blogposts')->everyFiveMinutes();
}

A new problem

We’ve now got a new issue, if we manually unpublish an article, it’ll publish itself again the next time we run the command. So what’s the solution?

Simply put, you could create another column via a migration called deleted_at and update the code we’ve done in the controller so it’s:

public function publish()
{
    $count = 0;
    $articles = BlogPost::where('status', BlogPost::STATUS_UNPUBLISHED)
                ->whereDate('published_at', '<=', Carbon::now())
								->whereDate('deleted_at', '>=', Carbon::now())
                ->get();

    foreach ($articles as $article) {
        $article->status = BlogPost::STATUS_PUBLISHED;
        $article->save();
        $count++;
    }
    return $count;
}

This has the added benefit of allowing you to also have your articles delete themselves on a schedule.

I've got the skills to help

just let me know how

I've been in the industry professionally for more than 10 years, and developing for over 25. I've made APIs for thousands of users and software for some of the UK's biggest brands.