Extending OctoberCMS – Building a Soft-Delete Plugin
Developers usually stick with a new CMS for its simplicity and extensibility. OctoberCMS presents itself as a back to basics CMS, and provides an enjoyable experience for both developers and users. In this article, I’m going to demonstrate some aspects of the CMS that make it extensible, and we’ll also try a simple plugin to extend another plugin functionality.
Introduction
Every CMS has a plugin system for extending the platform’s functionality, and we measure the extensibility by how deep into the CMS’ inner workings we can go. However, we’re not only talking about the CMS here, we’re talking about plugins!
If you build a plugin, you need to make sure that other developers can change bits of your functionality. For example, we have a blog plugin and the user can publish a post by selecting it on a list. It would be a good idea to fire an event saying that a new post has been published, and another developer may hook into this event and notify subscribed users via email about this!
class Posts extends Controller
{
public function index_onPublish()
{
if (($checkedIds = post('checked')) && is_array($checkedIds) && count($checkedIds)) {
foreach ($checkedIds as $postId) {
if ((!$post = Post::find($postId)) || !$post->canEdit($this->user))
continue;
$post->publish();
Event::fire('rainlab.blog.posts.published', [$post]);
}
Flash::success('Successfully published those posts.');
}
return $this->listRefresh();
}
}
The other developer can listen for this event to do something with the published post.
Event::listen('rainlab.blog.posts.published', function($post) {
User::subscribedTo($post).each(function($user) use($post) {
Mail::send('emails.notifications.post-published', ['user' => $user, 'post' => $post], function($message) use($user, $post) {
$message->from('us@example.com', 'New post by ' . $user->name);
$message->to($user->email);
});
});
});
We will mainly use events to hook into different parts of the request cycle. Let’s start with a concrete example to clear things up.
The Rainlab Blog Plugin
If you’ve used OctoberCMS for a while, you must know the Rainlab Blog plugin. It lets you add posts and attach them to categories in the backend, and you can display them in the front-end using components.
On the posts listing page, we have the possibility to delete posts. But, what if we wanted to soft delete them? Let’s see if we can do that and learn more about OctoberCMS extensibility.
Creating a New Plugin
Go ahead and create a new plugin for our demo using the scaffolding helper command, and update the plugin details inside the Plugin.php
file.
php artisan create:plugin rafie.blogplus
Extending the Database Schema
The first thing that comes to mind when talking about soft deletion is the deleted_at
field column that need to be present in the DB.
Create a new file called create_posts_deleted_at_field.php
under the blogplus/updates
folder and update the version.yaml
file.
// updates/version.yaml
1.0.1:
- First version of blogplus.
- create_posts_deleted_at_field.php
// updates/create_posts_deleted_at_field.php
class CreatePostsDeletedAtField extends Migration
{
public function up()
{
Schema::table('rainlab_blog_posts', function ($table) {
$table->timestamp('deleted_at')
->default(null)
->nullable();
});
}
public function down()
{
Schema::table('rainlab_blog_posts', function ($table) {
$table->dropColumn('deleted_at');
});
}
}
The migration class will alter the rainlab_blog_posts
table and add our deleted_at
column with a default null value. Don’t forget to run the php artisan plugin:refresh rafie.blogplus
command for the changes to take effect.
Extending Posts List
Next, we have to add our field as a column to be displayed in the list. OctoberCMS provides an event for us to hook into, and alter the currently displayed widget (the backend list is considered a widget).
Event::listen('backend.list.extendColumns', function ($widget) {
// Only for the Posts controller
if (( ! $widget->getController() instanceof \Rainlab\Blog\Controllers\Posts)) {
return;
}
$widget->addColumns([
'deleted_at' => [
'label' => 'Deleted',
'type' => 'date'
]
]);
});
Note: The above code should be placed inside the Plugin@boot
method.
We have an if statement to prevent our code from being executed on every page, then we add a new column to the list widget, and we can also remove any existing ones using the removeColumn
method. Check the documentation for a list of available column options.
Extending the Filter
The bar on top of the posts list lets the user filter the list using a date, category, etc. In our case, we need a filter to show/hide the trashed posts.
// plugin.php
Event::listen('backend.filter.extendScopes', function ($widget) {
// Only for the Posts controller
if (( ! $widget->getController() instanceof \Rainlab\Blog\Controllers\Posts)) {
return;
}
$widget->addScopes([
'Trashed' => [
'label' => 'Hide trashed',
'type' => 'checkbox',
'scope' => 'trashed'
],
]);
});
You can read more about the list filters in the documentation. The above code is fairly simple, and only contains a few options. However, the scope
attribute should be the name of a query scope method defined inside the related (Models\Post
) model instance.
Extendable Classes
The October\Rain\Extension\ExtendableTrait
trait provides a magical way to dynamically extend an existing class by adding new methods, properties, behavior, etc. In our example we need to add a new method to the posts model to handle our scope filter.
// plugin.php
\Rainlab\Blog\Models\Post::extend(function ($model) {
$model->addDynamicMethod('scopeTrashed', function ($query) {
return $query->where('deleted_at', '=', null);
});
});
We can do the same with addDynamicProperty
, asExtension
, etc. Let’s refresh our posts list to see if our changes are working.
Of course, we don’t have any trashed posts yet because we need to finish the last part: intercepting the deletion of the post and only updating the deleted_at
column.
Tip: Instead of using the scope
attribute, you can use conditions
to specify a simple where condition. The code below gives the same result as using model scopes.
$widget->addScopes([
'Trashed' => [
'label' => 'Hide trashed',
'type' => 'checkbox',
'conditions' => 'deleted_at IS NULL',
],
]);
Eloquent Events
Eloquent fires a list of events on every action (create, update, delete, etc). In this case, we need to hook into the delete event and prevent the record’s deletion.
When deleting a record, the deleting
event is fired before performing the actual delete action, and the deleted
one is fired afterwards. If you return false
on the deleting
event, the action will abort.
// plugin.php
Event::listen('eloquent.deleting: RainLab\Blog\Models\Post', function ($record) {
$record->deleted_at = Carbon::now();
$record->save();
return false;
});
Now we are ready to test the final result! Go ahead and delete some records, and then go to the post listing page to see if you can toggle the trashed items in the list.
Conclusion
This article was a quick overview of how you can extend different parts of the OctoberCMS platform. You can read more about this in the extending plugins section of the documentation. If you have any questions or comments, be sure to post them below!