OctoberCMS CRUD – Building a Team/Project Management Plugin

Share this article

So far, we covered different aspects of OctoberCMS. This is a follow up article to discover how to use OctoberCMS for CRUD applications and take a detailed view at how to work with models, relations and controllers. Let’s get started.

OctoberCMS logo

Requirements

I will assume you already know how to set up a working installation of OctoberCMS. If not, you can check out the introduction article, or read the installation section in the documentation.

What We’re Building

We are going to build a project management plugin where you can add different users to teams and assign them to projects. You can check the final result on GitHub, and feel free to suggest edits or additions to the plugins.

Setting up the Plugin

Let’s start by using the create:plugin scaffolding command to create the initial plugin structure, and define the Plugin::pluginDetails method with our plugin details.

php artisan create:plugin rafie.sitepointDemo
// Plugin.php

public function pluginDetails()
{
    return [
        'name'        => 'Project management',
        'description' => 'Manage your teams and projects.',
        'author'      => 'RAFIE Younes',
        'icon'        => 'icon-leaf'
    ];
}

Creating Database Tables

Every team has a name, a list of users and projects.

php artisan create:model rafie.sitepointdemo Team
// models/team.php

class Team extends Model
{
    // ...
    public $table = 'rafie_sitepointDemo_teams';
    
    public $hasMany = [
        'projects'  => '\Rafie\SitepointDemo\Projects',
        'users'      => '\Backend\Models\User'
    ];
    
    // ...
}
// updates/create_teams_table.php

class CreateTeamsTable extends Migration
{

    public function up()
    {
        Schema::create('rafie_sitepointDemo_teams', function($table)
        {
            $table->engine = 'InnoDB';
            $table->increments('id');
            $table->string('name', 100);
            $table->timestamps();
        });
    }

    public function down()
    {
        Schema::dropIfExists('rafie_sitepointDemo_teams');
    }
}

Every project belongs to a team and has a name, a description and an end date.

php artisan create:model rafie.sitepointdemo Project
// models/project.php

class Project extends Model
{
    
    // ...
    
    public $table = 'rafie_sitepointDemo_projects';
    
    public $belongsTo = [
        'team' => '\Rafie\SitepointDemo\Models\Team'
    ];
    
    // ...
}
// updates/create_projects_table.php

class CreateProjectsTable extends Migration
{

    public function up()
    {
        Schema::create('rafie_sitepointDemo_projects', function($table)
        {
            $table->engine = 'InnoDB';
            $table->increments('id');
            $table->string('name', 100);
            $table->text('description');
            $table->datetime('ends_at');
            $table->integer('team_id')->unsigned();
            $table->timestamps();
        });
    }
    
    // ...
}

Because OctoberCMS already has a users table for the backend, we should add the team_id column. We will create a migration to add our team index.

// updates/add_team_to_users.php

class AddTeamToUsers extends Migration
{
    public function up()
    {
        if(!Schema::hasColumn('backend_users', 'team_id'))
        {
            Schema::table('backend_users', function($table)
            {
                $table->integer('team_id')->unsigned()->index()->nullable();
            });
        }
    }

    public function down()
    {
        if(Schema::hasColumn('backend_users', 'team_id'))
        {
            Schema::table('backend_users', function($table)
            {
                $table->dropColumn('team_id');
            });
        }
    }
}

Then, we need to link to it with a new relationship definition.

// Plugin.php

class Plugin extends PluginBase
{
    // ...
    public function boot()
    {
        User::extend(function($model){
            $model->belongsTo['team'] = ['Rafie\SitepointDemo\Models\Team'];
        });
    }
}

Our plugin version file looks like this:

// updates/version.yaml

1.0.1: 
    - First version of Sitepoint demo
    - add_team_to_users.php
    - create_teams_table.php
    - create_projects_table.php

Managing Teams

Inside your models folder, you can see that every model class has a configuration folder which contains two files:

  • columns.yaml: Holds the table columns that you want to use when listing table records.
  • fields.yaml: The same thing as columns but it’s used to configure forms for creating and updating records.

To manage teams we need to create a controller to take action on certain events. We use the following scaffolding command.

php artisan create:controller rafie.sitepointDemo Teams

If you follow the naming conventions, the controller will automatically map to the model. If you check in your browser at the backend/rafie/sitepointDemo/teams/create URL, you’ll see the new record form.

Inside config_form.yaml, you’ll see that the form and modelClass properties are mapped to our team model. The new team form only shows a disabled input for the ID. We can add other inputs using the fields.yaml file inside our model.

// models/team/fields.yaml

fields:
    name:
        label: Name
        type: text
        required: true
    users:
        label: Users
        type: checkboxlist

Every team has a name and a list of users. The name is a simple text value, while the users are listed using the checkboxlist component. You can check the list of field types in the documentation.

You have also the ability to use form widgets in the field types. Widgets are rich components like a WYSWYG editor, Color picker, Media Finder, etc. The only part left here is to create a Team::getUsersOptions method to fill the users checkbox list.

// models/team.php

class Team extends Model
{
    // ...
    public function getUsersOptions()
    {
        return \Backend\Models\User::lists('login', 'id');
    }
}

Create team

You can see from the screenshot above that the fields marked as required have an asterisk after the input name. However, this does not mean that the validation is handled for you. You still need to add validation inside your models.

// models/Team.php

class Team extends Model
{
    use \October\Rain\Database\Traits\Validation;

    public $rules = [
        'name' => 'required'
    ];

    // ...
}

The fields in the configuration file are automatically mapped to the model if found. If not, we need to use the create_onSave and update_onSave methods to alter the saving strategy.

// controllers/teams.php

class Teams extends Controller
{
    // ...
    public function create_onSave()
    {
        $inputs = post('Team');

        // save team
        $teamModel = new \Rafie\SitepointDemo\Models\Team;
        $teamModel->name = $inputs['name'];
        $teamModel->save();

        // update users team_id
        \Backend\Models\User::whereIn('id', $inputs['users'])
                            ->update(['team_id' => $teamModel->id]);

        \Flash::success("Team saved successfully");
        
        return $this->makeRedirect('update', $teamModel);
    }
}

The post method is a helper function to avoid resolving the request object from the container. After saving the team model and updating the user relation we show a success message and create a redirect response using the FormController::makeRedirect method.

// controllers/teams.php

class Teams extends Controller
{
    // ...
    public function update_onSave($recordId)
    {
        $inputs = post('Team');

        // update team
        $teamModel = \Rafie\SitepointDemo\Models\Team::findOrFail($recordId);
        $teamModel->name = $inputs['name'];
        $teamModel->save();

        \Backend\Models\User::where('team_id', $teamModel->id)
                            ->update(['team_id' => 0]);

        // update users team_id
        \Backend\Models\User::whereIn('id', $inputs['users'])
                            ->update(['team_id' => $teamModel->id]);

        \Flash::success("Team updated successfully");
    }
}

The update_onSave method has one parameter containing the updated record ID. We update the team and the attached users accordingly. Another way to accomplish this is to make use of the FormController::update_onSave method. It takes care of mapping form fields to the model and saving it.

// controllers/teams.php

class Teams extends Controller
{
    // ...
    public function update_onSave($recordId)
    {
        $inputs = post('Team');

        \Backend\Models\User::where('team_id', $recordId)
                            ->update(['team_id' => 0]);

        // update users team_id
        \Backend\Models\User::whereIn('id', $inputs['users'])
                            ->update(['team_id' => $recordId]);

        $this->asExtension('FormController')->update_onSave($recordId, $context);
    }
}

The only part left is deleting the records. You may use the update_onDelete method to reset the team_id on the users table and then delete the team.

// controllers/teams.php

class Teams extends Controller
{
    // ...
    public function update_onDelete($recordId)
    {
        $teamModel = \Rafie\SitepointDemo\Models\Team::findOrFail($recordId);
        \Backend\Models\User::where('team_id', $teamModel->id)
                            ->update(['team_id' => 0]);
        $teamModel->delete();
        \Flash::success("Team deleted successfully");

        return $this->makeRedirect('delete', $teamModel);
    }
}

Or you could just use formAfterDelete to reset the team_id.

// controllers/teams.php

class Teams extends Controller
{
    // ...
    public function formAfterDelete($model)
    {
        \Backend\Models\User::where('team_id', $model->id)
                            ->update(['team_id' => 0]);
    }
}

If you noticed, the update form doesn’t automatically select the users attached to the team. We may have to do it manually using the formExtendFields method inside the Teams controller. The getContext method returns whether the user is creating or updating the model.

// controllers/teams.php

class Teams extends Controller
{
    // ...
    public function formExtendFields($form)
    {
        if( $form->getContext() === 'update')
        {
            $team = $form->model;
            $userField = $form->getField('users');
            $userField->value = $team->users->lists('id');
        }
    }
}

Managing Projects

We follow the same steps for managing projects: we start by defining the form fields.

models/project/fields.yaml

fields:
    name:
        label: Name
        type: text
        required: true
    description:
        label: Description
        type: textarea
        required: true
    ends_at:
        label: Ends At
        type: datepicker
        required: true
    team_id:
        label: Team
        type: dropdown

We define the list of teams to be displayed on the team dropdown.

// models/project.php

class Project extends Model
{
    // ...
    public function getTeamIdOptions()
    {
        $teams = \Rafie\SitepointDemo\Models\Team::all(['id', 'name']);
        $teamsOptions = [];

        $teams->each(function($team) use (&$teamsOptions) {
            $teamsOptions[$team->id] = $team->name;
        });

        return $teamsOptions;
    }
}

Create Project

Because all form fields are mapped to the model, we won’t have to hook into the saving process to update some relations. The create, update and delete actions are handled automatically in this case.

Listing

OctoberCMS makes listing records very simple and extendable. You can show and hide columns, search, sort, filter, format column values, etc. Check the documentation for the full list of options.

Listing Teams

The controllers/teams/config_list.yaml file contains our listing options. Every property has a comment describing its usage.

// controllers/teams/config_list.yaml

# Model List Column configuration
list: $/rafie/sitepointdemo/models/team/columns.yaml

# Model Class name
modelClass: Rafie\SitepointDemo\Models\Team

# List Title
title: Manage Teams

# Link URL for each record
recordUrl: rafie/sitepointdemo/teams/update/:id

# Message to display if the list is empty
noRecordsMessage: backend::lang.list.no_records

# Records to display per page
recordsPerPage: 20

# Displays the list column set up button
showSetup: true

# Displays the sorting link on each column
showSorting: true

//...

You can see that the list property points to the columns.yaml file, which defines the columns that should be displayed. The showSetup option lets the user select what columns to show in the list.

List Setup

// models/team/columns.yaml

columns:
    id:
        label: ID
        searchable: true
    name:
        label: Name
    users:
        label: Users
        relation: users
        select: login
        searchable: false

The id and name properties are self-explanatory. The searchable property is set to true by default, that’s why we didn’t specify it on the name property.

OctoberCMS has a relation property which can be used to show values from related models. In this case, we select the login attribute from the users relation model. You can check the documentation for the full list of available column options.

Teams List

Listing Projects

We follow the same steps for listing projects: we have a name, a description, an end date and a team.

// models/project/columns.yaml

columns:
    id:
        label: ID
        searchable: true
    name:
        label: Name
    description:
        label: Description
        type: text
    ends_at:
        label: End At
        type: datetime
    team:
        label: Team
        relation: team
        select: name

To help format the end date properly inside our list, we specify the column type as datetime. This may throw an exception for you because you need to add the ends_at attribute in your model inside the $dates array. Check the documentation for the full list of available column types.

// models/project.php

class Project extends Model
{
    // ...
    protected $dates = ['ends_at'];
    // ...
}

As for the team column, we specify the relation type and select the name attribute from the model. The final result looks like this.

Projects listing

Extending Lists

If you want to alter the list behavior, you may override the index method inside your controller and the index.htm view. What we want to do now is truncate the description column. You can check the documentation for more details about extending the list behavior.

// controllers/projects.php

class Projects extends Controller
{
    // ...
    public function listOverrideColumnValue($record, $columnName)
    {
        if( $columnName == "description" && strlen($record->description) > 20 )
        {
            $description = substr($record->description, 0, 20);

            return "<span title='{$record->description}'>{$description}...</span>";
        }
    }
}

You may be thinking about extending the users listing to show their current team. We use the boot method inside our plugin definition file to extend other plugins.

// Plugin.php

class Plugin extends PluginBase
{
    // ...
    public function boot()
    {
        // ...
        
        \Backend\Controllers\Users::extendListColumns(function ($list) {
            $list->addColumns([
                'team' => [
                    'label' => 'Team',
                    'relation' => 'team',
                    'select' => 'name'
                ]
            ]);
        });
    }
}

Listing Users

Filters

Filtering lists in OctoberCMS is easy. First, you reference your filter configuration file inside your config_list.yaml file.

// controllers/projects/config_list.yaml

// ...
filter: config_filter.yaml
// ...

Inside your config_filter.yaml file, you define a list of scopes that you want to use.

// controllers/projects/config_filter.yaml

scopes:
    team:
        label: Team
        modelClass: \Rafie\SitepointDemo\Models\Team
        nameFrom: name
        conditions: team_id = :filtered

Our scope is named team and will list our available teams using the specified modelClass and nameFrom properties. The conditions will filter projects where the team_id is equal to the selected teams; you may think of it as a raw SQL where statement. The screenshot below shows the list of projects taken by the Backend team.

Team filtered projects

OctoberCMS has two scope types. The first one is the group type, and it’s the one we used previously. The second is the checkbox type, which is used for boolean situations. We’ll use the latter to hide the past due projects from the list.

// controllers/projects/config_filter.yaml

scopes:
    // ...
    hide_past_due:
        label: Hide past due
        type: checkbox
        conditions: ends_at > now()

The only past due project in our list is the one with an id of 2.

Past due filtered projects

Permissions

We can’t talk about CRUD operations without covering permissions and how to guard your controllers. Combined with the simplicity of Laravel, OctoberCMS lets you define a list of permissions for your plugin and group them under a specific tab, then use them to guard your controllers.

// Plugin.php

class Plugin extends PluginBase
{
    // ...
    public function registerPermissions()
    {
        return [
            'rafie.sitepointDemo.manage_teams' => [
                'label' => 'Manage Teams',
                'tab' => 'SitepointDemo'
            ],
            'rafie.sitepointDemo.manage_projects' => [
                'label' => 'Manage Projects',
                'tab' => 'SitepointDemo'
            ]
        ];
    }
}

If you visit the update or create user page and select the permissions tab, you’ll see the SitepointDemo tab which holds the plugin permissions.

User Permissions

Now, inside the projects and teams controllers you add the $requiredPermission attribute.

// controllers/teams.php

class Teams extends Controller
{
    // ...
    public $requiredPermissions = ['rafie.sitepointDemo.manage_teams'];
    // ...
}
// controllers/projects.php

class Projects extends Controller
{
    // ...
    public $requiredPermissions = ['rafie.sitepointDemo.manage_projects'];
    // ...
}

If you want to restrict access to a certain action inside a controller, you may use the User::hasAccess and User::hasPermissions methods. Check the documentation for more details about permissions.

// controllers/teams.php

class Teams extends Controller
{
    // ...
    public function update()
    {
        if( !$this->user->hasPermissions(['rafie.sitepointDemo.update_teams']) )
        {
            // redirect Unauthorized 401
        }
    }
}

Conclusion

Every CMS tries to make CRUD operations easier and more straightforward for newcomers, and I think that OctoberCMS achieved that goal successfully by making every aspect of it clear and extendable.

You can check the final result on GitHub and if you have any questions or opinions let me know in the comments!

Frequently Asked Questions (FAQs) about OctoberCMS CRUD and Team/Project Management Plugin

How Can I Integrate OctoberCMS CRUD with Microsoft Teams?

Integrating OctoberCMS CRUD with Microsoft Teams involves using the Microsoft Teams API. This API allows you to create, update, and delete teams and channels, send messages, and more. You’ll need to register your application in the Azure portal, get the necessary permissions, and then use the API endpoints to perform the desired actions. You can use PHP or any other language that supports HTTP requests to interact with the API.

What Are the Benefits of Using OctoberCMS for Team/Project Management?

OctoberCMS offers a simple and intuitive interface for managing teams and projects. It allows you to create, read, update, and delete (CRUD) records easily. It also supports user roles and permissions, which means you can control who has access to what. Moreover, it’s built on Laravel, a popular PHP framework, which ensures robustness and scalability.

Can I Use Webhooks with OctoberCMS CRUD?

Yes, you can use webhooks with OctoberCMS CRUD. Webhooks allow you to send real-time updates to other applications or services. For example, you can send a notification to Microsoft Teams whenever a new record is created in OctoberCMS. To do this, you’ll need to create a webhook in Microsoft Teams and then configure OctoberCMS to send a POST request to the webhook URL whenever a record is created.

How Can I Authenticate My Application with Microsoft Teams API?

Authenticating your application with Microsoft Teams API involves obtaining an access token from Azure Active Directory (AD). You’ll need to register your application in the Azure portal, configure the necessary permissions, and then use the OAuth 2.0 authorization code flow to get an access token. This token is then included in the Authorization header of your API requests.

How Can I Send Messages to Microsoft Teams from OctoberCMS?

Sending messages to Microsoft Teams from OctoberCMS involves using the Microsoft Teams API. You’ll need to create a channel in Teams, get the channel ID, and then use the send message API endpoint to send a message to the channel. You can include text, images, and other content in your messages.

Can I Use OctoberCMS CRUD for Large Teams and Projects?

Yes, OctoberCMS CRUD is suitable for large teams and projects. It’s built on Laravel, a robust and scalable PHP framework. It also supports pagination, which means you can easily manage large amounts of data. Moreover, it allows you to define user roles and permissions, which is essential for managing large teams.

How Can I Add Incoming Webhooks to Microsoft Teams?

Adding incoming webhooks to Microsoft Teams involves creating a webhook in the Teams channel where you want to receive the notifications. You’ll need to go to the channel settings, select the “Connectors” option, and then choose “Incoming Webhook”. You can then configure the webhook and get the webhook URL.

Can I Use OctoberCMS CRUD with Other APIs?

Yes, you can use OctoberCMS CRUD with other APIs. It’s a flexible and extensible system that allows you to interact with any API that supports HTTP requests. You can use it to create, read, update, and delete records in any system that provides an API, such as Microsoft Teams, Slack, Google Workspace, and more.

How Can I Handle Errors in OctoberCMS CRUD?

Handling errors in OctoberCMS CRUD involves using try-catch blocks in your code. When an error occurs, an exception is thrown, which you can catch and handle appropriately. You can also use the Laravel validation system to validate data before it’s saved to the database, which can prevent many common errors.

Can I Customize the OctoberCMS CRUD Interface?

Yes, you can customize the OctoberCMS CRUD interface. It’s built with Twig, a flexible and powerful templating engine. You can create custom views, add your own CSS and JavaScript, and even create your own plugins to extend the functionality of the system.

Younes RafieYounes Rafie
View Author

Younes is a freelance web developer, technical writer and a blogger from Morocco. He's worked with JAVA, J2EE, JavaScript, etc., but his language of choice is PHP. You can learn more about him on his website.

BrunoScmscontent management systemcrudoctobercmsOOPHPpermissionsPHPpluginplugin developmentplugins
Share this article
Read Next
Get the freshest news and resources for developers, designers and digital creators in your inbox each week