Storing data in a plugin database

This tutorial covers the matter of storing data in a specific data table created by the plugin. For this purpose, we create a data base model in the source code of the plugin and run a migration when deploying the plugin. We then need a contract and a related repository to define the functions for creating, reading, updating or deleting data from the data table. In the ContentController, we use these functions to access the data base and make the data available in the template.

In the example plugin below, we create a simple To Do list. The plugin has the following features:

  • Get all tasks of a logged in customer
  • Add new tasks to the list
  • Mark existing tasks as done
  • Delete tasks that are marked as done

Step 1: Creating the plugin files

We need a total of 12 different files for this plugin. Comparable to the plugins we created in the other tutorials, this plugin requires a plugin.json containing the general plugin information. The source code of our plugin is stored in 8 individual files. Furthermore, we need a template file to display the plugin in the browser, a CSS file that contains the styling, and a script file for dynamically updating the To Do list without reloading the entire browser window every time. Create the folder structure and the required files. Pay attention to the scheme given below.

ToDoList/
    ├── resources/
    │   ├── css/
    │   │   └── main.css
    │   │
    │   ├── js/
    │   │   └── todo.js
    │   │
    │   └── views/
    │       └── content/
    │           └── todo.twig
    │
    ├── src/
    │   ├── Contracts/
    │   │   └── ToDoRepositoryContract.php
    │   │
    │   ├── Controllers/
    │   │   └── ContentController.php
    │   │
    │   ├── Migrations/
    │   │   └── CreateToDoTable.php
    │   │
    │   ├── Models/
    │   │   └── ToDo.php
    │   │
    │   ├── Providers/
    │   │   ├── ToDoServiceProvider.php
    │   │   └── ToDoRouteServiceProvider.php
    │   │
    │   ├── Repositories/
    │   │   └── ToDoRepository.php
    │   │
    │   └── Validators/
    │       └── ToDoValidator.php
    │
    ├── plugin.json // plugin information
    └── // additional files (Readme, license etc.)

Step 2: Filling the source files

After creating all folders and files, we start by filling the files that contain the plugin source code. Copy the code into the respective file. The code is explained in detail below.

Code for the Model

This is the model for our data base table. The table will have 5 columns for storing the following data:

  • The ID of the task
  • A string with the description of the task
  • The ID of the user who adds the task
  • The status showing if the task is done
  • A timestamp showing when the task was created
ToDoList/src/Models/ToDo.php
<?php

namespace ToDoList\Models;

use Plenty\Modules\Plugin\DataBase\Contracts\Model;

/**
 * Class ToDo
 *
 * @property int     $id
 * @property string  $taskDescription
 * @property int     $userId
 * @property boolean $isDone
 * @property int     $createdAt
 */
class ToDo extends Model
{
    /**
     * @var int
     */
    public $id              = 0;
    public $taskDescription = '';
    public $userId          = 0;
    public $isDone          = false;
    public $createdAt       = 0;

    /**
     * @return string
     */
    public function getTableName(): string
    {
        return 'ToDoList::ToDo';
    }
}

Code for the Migration

Next, we create a migration class that must be specified in the runOnBuild attribute of the plugin.json file.

ToDoList/src/Migrations/CreateToDoTable.php
<?php

namespace ToDoList\Migrations;

use ToDoList\Models\ToDo;
use Plenty\Modules\Plugin\DataBase\Contracts\Migrate;

/**
 * Class CreateToDoTable
 */
class CreateToDoTable
{
    /**
     * @param Migrate $migrate
     */
    public function run(Migrate $migrate)
    {
        $migrate->createTable(ToDo::class);
    }
}

Code for the plugin.json

ToDoList/plugin.json
{
	"name":"ToDoList",
	"description":"A simple To Do list",
	"namespace":"ToDoList",
	"author":"Your name",
	"type":"template",
	"serviceProvider":"ToDoList\\Providers\\ToDoServiceProvider",
	"runOnBuild":["ToDoList\\Migrations\\CreateToDoTable"]
}

Code for the Contract

A contract is a PHP interface in the Laravel framework. The ToDoRepositoryContract will be the interface for our repository.

ToDoList/src/Contracts/ToDoRepositoryContract.php
<?php

namespace ToDoList\Contracts;

use ToDoList\Models\ToDo;

/**
 * Class ToDoRepositoryContract
 * @package ToDoList\Contracts
 */
interface ToDoRepositoryContract
{
    /**
     * Add a new task to the To Do list
     *
     * @param array $data
     * @return ToDo
     */
    public function createTask(array $data): ToDo;

    /**
     * List all tasks of the To Do list
     *
     * @return ToDo[]
     */
    public function getToDoList(): array;

    /**
     * Update the status of the task
     *
     * @param int $id
     * @return ToDo
     */
    public function updateTask($id): ToDo;

    /**
     * Delete a task from the To Do list
     *
     * @param int $id
     * @return ToDo
     */
    public function deleteTask($id): ToDo;
}

Code for the Validator

The next file on our list is the validator. The validator class will be used in the ToDoRepository.

ToDoList/src/Validators/ToDoValidator.php
<?php

namespace ToDoList\Validators;

use Plenty\Validation\Validator;

/**
 *  Validator Class
 */
class ToDoValidator extends Validator
{
    protected function defineAttributes()
    {
        $this->addString('taskDescription', true);
    }
}

Code for the Repository

In our repository we implement the functions specified in the contract. Here, we also access our data base table by using the Plenty\Modules\Plugin\DataBase\Contracts\DataBase contract.

ToDoList/src/Repositories/ToDoRepository.php
<?php

namespace ToDoList\Repositories;

use Plenty\Exceptions\ValidationException;
use Plenty\Modules\Plugin\DataBase\Contracts\DataBase;
use ToDoList\Contracts\ToDoRepositoryContract;
use ToDoList\Models\ToDo;
use ToDoList\Validators\ToDoValidator;
use Plenty\Modules\Frontend\Services\AccountService;


class ToDoRepository implements ToDoRepositoryContract
{
    /**
     * @var AccountService
     */
    private $accountService;

    /**
     * UserSession constructor.
     * @param AccountService $accountService
     */
    public function __construct(AccountService $accountService)
    {
        $this->accountService = $accountService;
    }

    /**
     * Get the current contact ID
     * @return int
     */
    public function getCurrentContactId(): int
    {
        return $this->accountService->getAccountContactId();
    }

    /**
     * Add a new item to the To Do list
     *
     * @param array $data
     * @return ToDo
     * @throws ValidationException
     */
    public function createTask(array $data): ToDo
    {
        try {
            ToDoValidator::validateOrFail($data);
        } catch (ValidationException $e) {
            throw $e;
        }

        /**
         * @var DataBase $database
         */
        $database = pluginApp(DataBase::class);

        $toDo = pluginApp(ToDo::class);

        $toDo->taskDescription = $data['taskDescription'];

        $toDo->userId = $this->getCurrentContactId();

        $toDo->createdAt = time();

        $database->save($toDo);

        return $toDo;
    }

    /**
     * List all items of the To Do list
     *
     * @return ToDo[]
     */
    public function getToDoList(): array
    {
        $database = pluginApp(DataBase::class);

        $id = $this->getCurrentContactId();
        /**
         * @var ToDo[] $toDoList
         */
        $toDoList = $database->query(ToDo::class)->where('userId', '=', $id)->get();
        return $toDoList;
    }

    /**
     * Update the status of the item
     *
     * @param int $id
     * @return ToDo
     */
    public function updateTask($id): ToDo
    {
        /**
         * @var DataBase $database
         */
        $database = pluginApp(DataBase::class);

        $toDoList = $database->query(ToDo::class)
            ->where('id', '=', $id)
            ->get();

        $toDo = $toDoList[0];
        $toDo->isDone = true;
        $database->save($toDo);

        return $toDo;
    }

    /**
     * Delete an item from the To Do list
     *
     * @param int $id
     * @return ToDo
     */
    public function deleteTask($id): ToDo
    {
        /**
         * @var DataBase $database
         */
        $database = pluginApp(DataBase::class);

        $toDoList = $database->query(ToDo::class)
            ->where('id', '=', $id)
            ->get();

        $toDo = $toDoList[0];
        $database->delete($toDo);

        return $toDo;
    }
}

Code for the ServiceProvider

ToDoList/src/Providers/ToDoServiceProvider.php
<?php

namespace ToDoList\Providers;

use Plenty\Plugin\ServiceProvider;
use ToDoList\Contracts\ToDoRepositoryContract;
use ToDoList\Repositories\ToDoRepository;

/**
 * Class ToDoServiceProvider
 * @package ToDoList\Providers
 */
class ToDoServiceProvider extends ServiceProvider
{
    /**
     * Register the service provider.
     */
    public function register()
    {
        $this->getApplication()->register(ToDoRouteServiceProvider::class);
        $this->getApplication()->bind(ToDoRepositoryContract::class, ToDoRepository::class);
    }
}

Code for the RouteServiceProvider

ToDoList/src/Providers/ToDoRouteServiceProvider.php
<?php

namespace ToDoList\Providers;

use Plenty\Plugin\RouteServiceProvider;
use Plenty\Plugin\Routing\Router;

/**
 * Class ToDoRouteServiceProvider
 * @package ToDoList\Providers
 */
class ToDoRouteServiceProvider extends RouteServiceProvider
{
    /**
     * @param Router $router
     */
    public function map(Router $router)
    {
        $router->get('todo', 'ToDoList\Controllers\ContentController@showToDo');
        $router->post('todo', 'ToDoList\Controllers\ContentController@createToDo');
        $router->put('todo/{id}', 'ToDoList\Controllers\ContentController@updateToDo')->where('id', '\d+');
        $router->delete('todo/{id}', 'ToDoList\Controllers\ContentController@deleteToDo')->where('id', '\d+');
    }

}

Code for the ContentController

ToDoList/src/Controllers/ContentController.php
<?php

namespace ToDoList\Controllers;

use Plenty\Plugin\Controller;
use Plenty\Plugin\Http\Request;
use Plenty\Plugin\Templates\Twig;
use ToDoList\Contracts\ToDoRepositoryContract;

/**
 * Class ContentController
 * @package ToDoList\Controllers
 */
class ContentController extends Controller
{
    /**
     * @param Twig                   $twig
     * @param ToDoRepositoryContract $toDoRepo
     * @return string
     */
    public function showToDo(Twig $twig, ToDoRepositoryContract $toDoRepo): string
    {
        $toDoList = $toDoRepo->getToDoList();
        $templateData = array("tasks" => $toDoList);
        return $twig->render('ToDoList::content.todo', $templateData);
    }

    /**
     * @param  \Plenty\Plugin\Http\Request $request
     * @param ToDoRepositoryContract       $toDoRepo
     * @return string
     */
    public function createToDo(Request $request, ToDoRepositoryContract $toDoRepo): string
    {
        $newToDo = $toDoRepo->createTask($request->all());
        return json_encode($newToDo);
    }

    /**
     * @param int                    $id
     * @param ToDoRepositoryContract $toDoRepo
     * @return string
     */
    public function updateToDo(int $id, ToDoRepositoryContract $toDoRepo): string
    {
        $updateToDo = $toDoRepo->updateTask($id);
        return json_encode($updateToDo);
    }

    /**
     * @param int                    $id
     * @param ToDoRepositoryContract $toDoRepo
     * @return string
     */
    public function deleteToDo(int $id, ToDoRepositoryContract $toDoRepo): string
    {
        $deleteToDo = $toDoRepo->deleteTask($id);
        return json_encode($deleteToDo);
    }
}

Step 3: Filling the resource files

For our To Do list plugin, we already created 3 files in the resources folder:

  • The todo.twig file in the views/content sub-folder with HTML structure and TWIG syntax. This template will be rendered in the browser.
  • The main.css file in the css sub-folder with the styling for our template
  • The todo.js file in the js sub-folder containing JavaScript code. The scripts in this file allow us to dynamically update the To Do list in the browser without reloading the entire page.

Code for the TWIG file

ToDoList/resources/views/content/todo.twig
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>To Do</title>

    <!-- Link main CSS file and additional fonts -->
    <link href="{{ plugin_path('ToDoList') }}/css/main.css" rel="stylesheet">
    <link href="https://fonts.googleapis.com/css?family=Open+Sans" rel="stylesheet">
    <link href="https://fonts.googleapis.com/css?family=Amatic+SC" rel="stylesheet">

    <!-- Integrate jQuery -->
    <script src="https://code.jquery.com/jquery-3.1.1.min.js" integrity="sha256-hVVnYaiADRTO2PzUGmuLJr8BLUSjGIZsDYGmIJLv2b8=" crossorigin="anonymous"></script>
</head>

<body>
    <!-- To Do list -->
    <div class="list">
        <h1 class="header">Things to do</h1>

        <ul class="tasks">
            {% if tasks is not null %}
                {% for task in tasks %}
                    <li>
                        <span class="task {% if task.isDone == 1 %} done {% endif %}">{{ task.taskDescription }}</span>
                        {% if task.isDone == 1 %}
                            <button id="{{ task.id }}" class="delete-button">Delete from list</button>
                        {% else %}
                            <button id="{{ task.id }}" class="done-button">Mark as done</button>
                        {% endif %}
                    </li>
                {% endfor %}
            {% endif %}
        </ul>

        <!-- Text field and submit button -->
        <div class="task-add">
            <input type="text" name="taskDescription" placeholder="Enter a new task here." class="input" autocomplete="off">
            <input type="submit" id="addTask" value="Add" class="submit">
        </div>
    </div>

    <!-- Enable adding, updating and deleting tasks in the To Do list without reloading the page -->
    <script src="{{ plugin_path('ToDoList') }}/js/todo.js"></script>
</body>
</html>

Code for the CSS file

ToDoList/resources/css/main.css
/* General styling */
body {
    background-color: #F8F8F8;
}

body,
input,
button{
    font:1em "Open Sans", sans-serif;
    color: #4D4E53;
}

a {
    text-decoration: none;
    border-bottom: 1px dashed #4D4E53;
}

/* List */
.list {
    background-color:#fff;
    margin:20px auto;
    width:100%;
    max-width:500px;
    padding:20px;
    border-radius:2px;
    box-shadow:3px 3px 0 rgba(0, 0, 0, .1);
    box-sizing:border-box;
}

.list .header {
    font-family: "Amatic SC", cursive;
    margin:0 0 10px 0;
}

/* Tasks */
.tasks {
    margin: 0;
    padding:0;
    list-style-type: none;
}

.tasks .task.done {
    text-decoration:line-through;
}

.tasks li,
.task-add .input{
    border:0;
    border-bottom:1px dashed #ccc;
    padding: 15px 0;

}

/* Input field */
.input:focus {
    outline:none;
}

.input {
    width:100%;
}

/* Done button & Delete button*/
.tasks .done-button {
    display:inline-block;
    font-size:0.8em;
    background-color: #5d9c67;
    color:#000;
    padding:3px 6px;
    border:0;
    opacity:0.4;
}

.tasks .delete-button {
    display:inline-block;
    font-size:0.8em;
    background-color: #77525c;
    color:#000;
    padding:3px 6px;
    border:0;
    opacity:0.4;
}

.tasks li:hover .done-button,
.tasks li:hover .delete-button {
    opacity:1;
    cursor:pointer;
}

/* Submit button */
.submit {
    background-color:#fff;
    padding: 5px 10px;
    border:1px solid #ddd;
    width:100%;
    margin-top:10px;
    box-shadow: 3px 3px 0 #ddd;
}

.submit:hover {
    cursor:pointer;
    background-color:#ddd;
    color: #fff;
    box-shadow: 3px 3px 0 #ccc;
}

Code for the JS file

ToDoList/resources/js/todo.js
// Add a new task to the To Do list when clicking on the submit button
$('#addTask').click(function(){
    var nameInput = $("[name='taskDescription']");
    var data = {
        'taskDescription': nameInput.val()
    };
    $.ajax({
        type: "POST",
        url: "/todo",
        data: data,
        success: function(data)
        {
            var data = jQuery.parseJSON( data );
            $("ul.tasks").append('' +
                '<li>' +
                '   <span class="task">' + data.taskDescription + '</span> ' +
                '   <button id="' + data.id + '"class="done-button">Mark as done</button>' +
                '</li>');
            nameInput.val("");
        },
        error: function(data)
        {
            alert("ERROR");
        }
    });
});

// Update the status of an existing task in the To Do list and mark it as done when clicking on the Mark as done button
$(document).on('click', 'button.done-button', function(e) {
    var button = this;
    var id = button.id;
    $.ajax({
        type: "PUT",
        url: "/todo/" + id,
        success: function(data)
        {
            var data = jQuery.parseJSON( data );
            if(data.isDone)
            {
                $("#" + id).removeClass("done-button").addClass("delete-button").html("Delete from list");
                $("#" + id).prev().addClass("done");
            }
            else
            {
                alert("ERROR");
            }
        },
        error: function(data)
        {
            alert("ERROR");
        }
    });
});

// Delete a task from the To Do list when clicking on the Delete from list button
$(document).on('click', 'button.delete-button', function(e) {
    var button = this;
    var id = button.id;
    $.ajax({
        type: "DELETE",
        url: "/todo/" + id,
        success: function(data)
        {
            $("#" + id).parent().remove();
        },
        error: function(data)
        {
            alert("ERROR");
        }
    });
});

All tasks are done

After creating the plugin, we have to add our new plugin to the plentymarkets inbox and deploy the plugin. Finally, to display the To Do list, open a new browser tab and type in your domain adding /todo at the end. Have fun with your new plugin!