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.
Key Takeaways
- Utilize the `create:plugin` command to easily scaffold and set up the initial structure for a project management plugin, enhancing the setup process in OctoberCMS.
- Implement relational database tables for teams and projects within OctoberCMS using migrations, ensuring structured data storage and retrieval.
- Extend the OctoberCMS User model to include team associations, thereby integrating user management seamlessly within the team and project management context.
- Employ OctoberCMS’s powerful listing capabilities to manage and display teams and projects effectively, utilizing customization options for sorting, filtering, and displaying specific columns.
- Leverage OctoberCMS’s permission system to secure access to the plugin’s functionalities, ensuring that only authorized users can manage teams and projects.
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');
}
}
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;
}
}
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.
// 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.
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.
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'
]
]);
});
}
}
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.
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
.
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.
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 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.