Modules

Introduction

Ability to create custom modules is added in Concord CRM version 1.5.

Modules are packages of code that expand the core functionality of Concord CRM. By developing your own modules, you can enhance Concord CRM or add entirely new features.

Concord CRM is using the nWidart/laravel-modules Laravel package to provide support for modules.

By default, Concord CRM includes numerous core modules that are essential and cannot be disabled.

Maintenance

You are responsible for maintaining the modules you create, ensuring they remain compatible with the latest version of Concord CRM when major changes occur.

Concord CRM is still a new modular application. With each update, we aim to enhance the code and make it more modular. Due to this, there will be breaking changes, and you will need to adapt your modules accordingly.

Security

Concord CRM is built with the Laravel PHP framework, which follows security practices to ensure reliability.

When creating new modules, you must ensure that your modules are secure and utilize existing functions and methods within the Laravel PHP framework.

Selling Modules on CodeCanyon

If you want to create modules and sell them as an author on CodeCanyon, you are free to do so. However, keep in mind that due to our exclusivity agreement, you can ONLY sell the modules on CodeCanyon.

Selling Concord CRM modules on CodeCanyon means you are responsible for setting the appropriate price of the module, presenting the module, offering support, ensuring compatibility with the latest version changes, and developing your marketing strategy.

You cannot present us as a partner, either as an individual or a company. Publishing a module from your Envato account means you own all the copyright to the module and the code.

At our discretion, we may decide to market your module or recommend it to our customers.

Support for Module Creation

We will not offer support for creating modules. If you are certain that something is not working correctly and believe it to be a bug, you can open a support ticket at Concord CRM Support and provide more information. We will be happy to take a look.

However, please note that we do not provide guidance on how to create specific features. Module development support is not included. You are encouraged to figure things out on your own, or explore the code if you need specific functions.

Development Environment

You should ensure that the module is compatible with the minimum required PHP version for Concord CRM.

Before starting to module(s) development for Concord CRM, you will need a Laravel development environment in place (php, composer, node, npm etc...).

If you are not familiar with Laravel, we highly recommend to first read Laravel Installation Guide

After your development environment is configured, you will need to perform clean installation of Concord CRM.

  • Extract the Concord CRM files e.q. in ~/Herd/concordcrm.
  • Perform fresh Concord CRM installation.
  • Run composer update to install the dev dependencies.
  • Install NPM packages by running npm install.
  • Edit the .env file and set the APP_ENV config key to local.
  • Edit the .env file and set the APP_DEBUG config key to true.
  • Edit the .env file and set the MODULE_CACHE_ENABLED config key to false.
  • Edit the .env file and set the MODEL_CACHE_ENABLED config key to false.
  • Edit the .env file and set the IDENTIFICATION_KEY config key to module-development.
  • Clear cache by running the php artisan core:clear-cache command.

Find an example .env file for development purposes below, of course, you will need to adjust the env variables values as per your development environment:

APP_NAME="Concord CRM"
APP_ENV=local
# Make sure the "APP_KEY" is set.
APP_KEY=base64:...
APP_DEBUG=true
APP_URL=http://concordcrm.test
SANCTUM_STATEFUL_DOMAINS=concordcrm.test,localhost:${APP_PORT}
IDENTIFICATION_KEY=module-development

DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=concordcrm
DB_USERNAME=root
DB_PASSWORD=

CACHE_DRIVER=file
FILESYSTEM_DISK=local
QUEUE_CONNECTION=sync
SESSION_DRIVER=file
SESSION_LIFETIME=120

MAIL_MAILER=smtp
MAIL_HOST=127.0.0.1
MAIL_PORT=1025
MAIL_USERNAME=null
MAIL_PASSWORD=null
MAIL_ENCRYPTION=null
MAIL_FROM_ADDRESS="hello@example.com"
MAIL_FROM_NAME="${APP_NAME}"

DEBUGBAR_ENABLED=true
MODULE_CACHE_ENABLED=false
MODEL_CACHE_ENABLED=false

Create your First Module

All Concord CRM modules must be placed in the modules folder located in the root of your Concord CRM installation. Each module must have a unique folder name and a unique name defined in the module.json file.

To create a module use the following artisan command:

php artisan module:make ModuleName

Please note that the module name must be a studly case.

After the command is executed a module with basic starting code will be created in the modules directory, the full path of the module will be modules/[ModuleName].

Navigate to Settings -> Modules, you will be able to notice that the module is listed in the available modules list.

Module JSON file

In the root of your module, you will find a module.json file. The most common configuration keys you will need to edit in this file are:

  • version: This specifies your module's version.
  • description: A short description of your module.
  • providers: Your module's Laravel service providers to autoload.
  • requires_at_least: The minimum version of Concord CRM required for the module to function properly.

Assets

Please refer to the CSS & Javascript guides.

Module composer.json File

Your module's composer.json file is crucial for managing dependencies and ensuring compatibility with Concord CRM. Below are important guidelines to follow when configuring your module's composer.json.

1. Avoid Redundant Dependencies

Do not install dependencies that already exist in the root composer.json of Concord CRM. These dependencies should be used directly in your module code becauase they exist in core.

2. Managing Dependencies with Composer

To prevent installing the Laravel framework or related packages redundantly, use the provide configuration key in your module's composer.json.

When you require a specific Laravel package in your module, Composer may attempt to install the Laravel framework or other related packages even if they are already present in the root composer.json. This happens because Composer does not automatically recognize that these dependencies are satisfied by the root project. By using the provide key, you can explicitly tell Composer that these packages are already provided, preventing duplicate installations.

3. Shipping with Vendor Folder

When uploading a module to Codecanyon, ensure that the module is shipped with the vendor folder.

Example composer.json configuration for a module:

{
    "name": "vendor/module",
    "description": "Example module",
    "autoload": {
        "psr-4": {
            "Modules\ModuleName\": "app/",
            "Modules\ModuleName\Database\Factories\": "database/factories/",
            "Modules\ModuleName\Database\Seeders\": "database/seeders/"
        }
    },
    "autoload-dev": {
        "psr-4": {
            "Modules\ModuleName\Tests\": "tests/"
        }
    },
    "require": {},
    "provide": {
        "laravel/framework": "*",
        "illuminate/support": "*",
        ...
    }
}

Bootstrap Module

To bootstrap module and add hooks like enabling, enabled, disabling, disabled, deleting, deleted you should use the module bootstrap/module.php file.

use Modules\Core\Facades\Module;
use Illuminate\Contracts\Foundation\Application;

return Module::configure('invoices')
    ->enabled(function (Application $app) {
        // Activate any module services
    })
    ->disabled(function (Application $app) {
        // Deactivate any module services
    })
    ->deleted(function (Application $app) {
        // Perform clean up
    });

Of course, you are free to create your own modules event using Laravel events feature to provide ability developers listen for specific events or merge two or more modules together by communicating with events.

Localization

Concord CRM is fully localizeable using the Laravel localization features with few additions added specifically for Concord CRM requirements, you should ensure your module is fully translatable in different languages by using text via the localization feature helper functions.

This will ensure customer will be able to translate the module content in a different language using Concord CRM built-in translator. In most cases, you will develop the module in the en locale.

When you create a module using the module:make command, the scaffold will automatically create the lang folder in your module root directory and will register the language namespace using the module name as a namespace.

The namespace of the translation will be the module name in lower case, you can use Laravel localization helper function to retrieve translated text:

public function show()
{
    abort(Response::HTTP_CONFLICT, __('invoices::group.key'));
}

The group is a file in the locale directory e.q. modules/Invoices/lang/en/[GROUP].php.

Localizing Vue Components

Please refer to the CSS & Javascript guides.

Add Menu Items

To add menu items from module to the dashboard sidebar menu, in your module service provider add method registerMenuItems:

use Modules\Core\Facades\Menu;
use Modules\Core\Menu\MenuItem;

protected function registerMenuItems() : void
{
    Menu::register(
        MenuItem::make(__('modulename::group.key'), '/invoices', 'CurrencyDollar')
            ->position(15)
            ->badge(fn () => Model::count())
            ->badgeVariant('info')
    );
}

The first argument of the MenuItem class's make method is the item label, the second argument is the route to redirect to upon clicking, and the third argument is the icon of the menu item.

The icon must be one of the available icons in the Icons.vue file, excluding the "Icon" suffix.

Next, you need to add a front-end route via Vue Router to render a Vue component. This component will display the content for the added route when the menu item is clicked.

<!-- modules/[ModuleName]/resources/js/views/MyIndexComponent.vue-->
<template>Hello from MyIndexComponent.vue</template>

<script setup></script>
// modules/[ModuleName]/resources/app.js

import { translate } from "core/i18n";

import MyIndexComponent from "./views/MyIndexComponent.vue";

if (window.Innoclapps) {
    Innoclapps.booting(function (app, router) {
        router.addRoute({
            path: "/invoices",
            component: MyIndexComponent,
            meta: {
                title: translate("modulename::group.key"),
            },
        });
    });
}

Make sure that the development server is started by running the npm run dev command and visit the route added.

Add Custom Permissions

Concord CRM has roles and permissions features that can be used to authorize certain actions, using the Spatie laravel-permissions package.

You can register custom permissions specific to the module you are developing that administrator can choose when assigning a role.

In the module service provider boot method you can register permissions as shown below:

Innoclapps::permissions(function ($manager) {
    $group = ['name' => 'invoices', 'as' => 'invoices'];

    $manager->group($group, function ($manager){
        $manager->view('view', [
            'as' => __('core::role.capabilities.view'),
            'permissions' => [
                'view own invoices' => __('core::role.capabilities.owning_only'),
                'view all invoices' => __('core::role.capabilities.all', ['resourceName' => 'Invoices']),
                'view team invoices' => __('users::team.capabilities.team_only'),
            ],
        ]);

        $manager->view('edit', [
            'as' => __('core::role.capabilities.edit'),
            'permissions' => [
                'edit own invoices' => __('core::role.capabilities.owning_only'),
                'edit all invoices' => __('core::role.capabilities.all', ['resourceName' => 'Invoices']),
                'edit team invoices' => __('users::team.capabilities.team_only'),
            ],
        ]);

        $manager->view('delete', [
            'as' => __('core::role.capabilities.delete'),
            'revokeable' => true,
            'permissions' => [
                'delete own invoices' => __('core::role.capabilities.owning_only'),
                'delete any invoice' => __('core::role.capabilities.all', ['resourceName' => 'invoices']),
                'delete team invoices' => __('users::team.capabilities.team_only'),
            ],
        ]);

        $manager->view('bulk_delete', [
            'permissions' => [
                'bulk delete invoices' => __('core::role.capabilities.bulk_delete'),
            ],
        ]);
    });
});

After navigating to Settings -> Users -> Roles you will be able to see the registered permissions when creating/editing the role.

To use the permissions, you will need use the Laravel authorizations feature, you can either check directly in the code whether the user has permission or use Policy to perform specific authorizations.

To create module related policy use the module:make-policy artisan command, for example:

php artisan module:make-policy InvoicePolicy Invoices

Executing the command will create a InvoicePolicy.php file in the module app/Policies directory:

// modules/Invoices/app/Policies/InvoicePolicy.php

namespace Modules\Invoices\Policies;

use Illuminate\Auth\Access\HandlesAuthorization;

class InvoicePolicy
{
    use HandlesAuthorization;

    /**
     * Determine whether the user can view the invoice.
     */
    public function view(User $user, Invoice $invoice): bool
    {
        if ($user->can('view all invoices')) {
            return true;
        }

        if ((int) $invoice->user_id === (int) $user->id) {
            return true;
        }

        if ($invoice->user_id && $user->can('view team invoices')) {
            return $user->managesAnyTeamsOf($invoice->user_id);
        }

        return false;
    }
}

In your controller you can simply authorize the action regularly using Laravel authorization features:

// modules/Invoices/app/Http/Controllers/Api/InvoiceController.php

public function show(string $id)
{
    $invoice = Invoice::findOrFail($id);

    $this->authorize('view', $invoice);
}

Mailable Templates

Concord CRM has a mailable templates feature that the administrator can edit via the dashboard to suit their requirements. To create a mailable template, use the module:make-mailable-template command as shown below:

php artisan module:make-mailable-template OrderShipped Invoices

The OrderShipped.php file will be created in your module app/Mail directory, you will need to register the template in the module service provider:

use Modules\Core\Facades\SettingsMenu;
use Modules\Core\Settings\SettingsMenuItem;
use Modules\Core\Support\ModuleServiceProvider;

class InvoicesServiceProvider extends ModuleServiceProvider
{
    protected array $mailableTemplates = [
        \Modules\Invoices\Mail\OrderShipped::class,
    ];
}

You don't need to set a subject or any content for the mailable because the user will provide this information. However, it's recommended to add default content. This gives the user a starting point and helps them understand what kind of template they are editing.

To set default content, use the default method of the mailable template and return a new DefaultMailable instance.

use Modules\Core\MailableTemplate\DefaultMailable;

public static function default(): DefaultMailable
{
    return new DefaultMailable('Default content', static::name());
}

Now, navigate to Settings -> Mail Templates. You will see the previously created template in the list of available templates for all locales.

Use this mailable template as a regular Laravel mailable to send emails from anywhere in the module codebase.

namespace Modules\Invoices\Http\Controllers\Api;

use Modules\Core\Http\Controllers\ApiController;
use Modules\Invoices\Mail\OrderShipped;
use Modules\Invoices\Models\Order;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Mail;

class OrderShipmentController extends ApiController
{
    /**
     * Ship the given order.
     */
    public function store(Request $request): JsonResponse
    {
        $order = Order::findOrFail($request->order_id);

        // Ship the order...

        Mail::to($request->user())->send(new OrderShipped($order));

        return $this->response('', JsonResponse::HTTP_NO_CONTENT);
    }
}

Notifications

In most cases, to send emails and notifications to users, you will want to use the Laravel notification feature. Concord CRM already uses this feature with a few specific additions.

With the notifications feature, you can send notifications to multiple channels. For example, you can notify a user that an invoice is overdue via email and notification (bell).

To create a Concord CRM notification, use the module:make-notification command as shown below:

php artisan module:make-notification OrderShipped Invoices

The OrderShipped.php file will be created in your module app/Notifications directory, you will need to register the notification in the module service provider:

use Modules\Core\Facades\SettingsMenu;
use Modules\Core\Settings\SettingsMenuItem;
use Modules\Core\Support\ModuleServiceProvider;

class InvoicesServiceProvider extends ModuleServiceProvider
{
    protected array $notifications = [
        \Modules\Invoices\Notifications\OrderShipped::class,
    ];
}

Use this notification as regular Laravel notification to send from anywhere in the module codebase:

namespace Modules\Invoices\Http\Controllers\Api;

use Modules\Core\Http\Controllers\ApiController;
use Modules\Invoices\Notifications\OrderShipped;
use Modules\Invoices\Models\Order;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;

class OrderShipmentController extends ApiController
{
    /**
     * Ship the given order.
     */
    public function store(Request $request): JsonResponse
    {
        $order = Order::findOrFail($request->order_id);

        // Ship the order...
        $request->user()->notify(new OrderShipped($order));

        return $this->response('', JsonResponse::HTTP_NO_CONTENT);
    }
}

To send an email from the notification, you will need to use a previously created mailable template or regular Laravel mailable.

namespace Modules\Contacts\Notifications;

use Illuminate\Contracts\Queue\ShouldQueue;
use Modules\Invoices\Mail\OrderShipped as OrderShippedMailable;
use Modules\Invoices\Models\Order;
use Modules\Core\MailableTemplate\MailableTemplate;
use Modules\Core\Notification;

class UserAssignedToContact extends Notification implements ShouldQueue
{
    /**
     * Create a new notification instance.
     */
    public function __construct(protected Order $contact) {}

    /**
     * Get the mail representation of the notification.
     */
    public function toMail(object $notifiable): OrderShippedMailable&MailableTemplate
    {
        return (new OrderShippedMailable($this->order))->to($notifiable);
    }

    /**
     * Get the array representation of the notification.
     *
     * @return array<string, mixed>
     */
    public function toArray($notifiable): array
    {
        return [
            'path' => sprintf('/orders/%s', $this->order->id).
            'lang' => [
                'key' => 'invoices::order.notifications.shipped',
                'attrs' => [
                    'number' => $this->order->number,
                ],
            ],
        ];
    }
}

If the notification does not need to send an email, you must instruct Concord CRM that no email channel is available for this notification. This ensures that the email channel checkbox won't appear in the user notification settings.

/**
 * Provide the notification available delivery channels.
 */
public static function channels(): array
{
    return ['database', 'broadcast'];
}

Module Relations

To add a relation to a module you don't have access to, you should use the Laravel dynamic way for resolving relations.

use Modules\Invoices\Models\Order;
use Modules\Billing\Models\Customer;

Order::resolveRelationUsing('customer', function (Order $orderModel) {
    return $orderModel->belongsTo(Customer::class, 'customer_id');
});

Please refer to the Laravel documentation for more information.

Module Archive

When you are done developing the module and want to prepare it for the end customer, such as uploading it for sale on the CodeCanyon platform, you need to prepare the module .zip archive.

Before creating the archive, you will need to perform a few steps:

Preparing the Assets

If your module has assets, include them in the .zip archive so Concord CRM can copy them to the public directory upon activation. Assets are any static files that should exist in the public directory.

  1. In the module root directory, create a folder named public and move any assets there.
  2. To include Vite assets, run npm run build-isolated in the module root directory. This command will build the Vite assets to modules/[ModuleName]/public/build instead of public/ModuleName/build.

Please note that the build directory is the one that is used in your module service provider:

Innoclapps::vite('resources/js/app.js', 'modules/'.$this->moduleNameLower().'/build');

If for some reason you decided to use a different directory, you will need to adjust this directory as well in vite.config.js, packages.json and in your service provider as it was shown above.

Concord CRM will publish the assets in the public/modules/[ModuleName] directory after the module is enabled and will delete them when the module is disabled or deleted.

Vendor Folder

Since most customers installing Concord CRM don't have Composer on their server, include the vendor folder in the .zip archive. Concord CRM will autoload this folder automatically.

Creating Module Archive

Create a .zip archive including the module folder, the archive structure should be like this:

- Invoices (the module folder name)
-- app
-- database
-- module.json
-- ...

After creating the module .zip archive, always test if uploading, enabling, disabling, and deleting the module works fine and does not trigger any errors that may stop the customer installation from working.