Quick Start Guide

Version Date Notes By
0.2 2018-01-31 OUTDATED JFM
0.1 2016-10-26 Initial release JFM

OUTDATED!!

This document will quickly guide you through the creation of a simple contact management

Note: This guide assumes that you have the base framework ready and working. If not, you should first follow the Installation Guide.

Intro

SGIV10 works differently then our previous SGI version. Instead of having just a backend application where everything is processed (database calls, business logic, client views, etc..) on the server, we have separated the client side from the server side.

The backend is responsible for the data access layer and the frontend is responsible for the presentation layer.

This can be a little daunting at first, but after a while all will be smother.

The separation of concerns brings a lot of advantages, since we can create a totally diferent frontend without the need to change the backend, have developers working on the frontend while others work on the backend, have a RESTfull API out of the box, etc...

So everytime we need to add some kind of functionality, most of the time we need to work on boths ends.

In this guide we will be creating a simple contact managment feature.

Creating a simple contact management

1. Backend

Migrations

Migrations are a way to keep track of you database, since by default it will only apply the changes that where not yet applied, it means we no longer have to do it manually. Also, since migrations are database agnostic we usually don't need to worry about what kind of database server we are working with, as long as is supported by Laravel data abstraction layer.

  1. Lets create a migration so we can create the table in our database.

    On the console run the following command: php artisan make:migration create_t00_contacts_table

    This will create a migration file on the database/migrations folder and the file name will be prefixed by a timestamp.

    Something like this: Ex: 2016_06_20_105000_create_t00_contacts_table.php

    Note: the table name sould always be in plural

  2. Next, lets open the migration file and define our table structure.

    The migration file, has 2 methods: up and down. The up method is where we are going to add the fields that will represent our table.

    public function up()
    {
        Schema::create('t00_contacts', function (Blueprint $table) {
            $table->increments('id');
            $table->integer('number');
            $table->string('name');
            $table->string('email', 50);
            $table->timestamps();
            $table->unsignedInteger('status_id')->index();
    
            // foreign keys
            $table->foreign('status_id')->references('id')->on('t00_boolean_statuses');
        });
    }

    The down methods is used to revert the changes of the up method. In this case we will drop the table

    public function down()
    {
        Schema::drop('t00_contacts');
    }
  3. Now lets run the migration. This will run the up method and create the table in our database

    php artisan migrate


Lets Break it down

Schema::create('t00_contacts', function (Blueprint $table) {
});
Part Description
Schema Class that provides database agnostic support for creating and manipulating tables across all of Laravel's supported database systems.
create this is the method we use the creata a new table
't00_contacts' The name of the table we want to create
function (Blueprint $table) This is a closure (anonymous function) that receives a Blueprint object that may be used to define the new table
$table->increments('id');
$table->integer('number');
$table->string('name');
$table->string('email', 50);
$table->timestamps();
$table->unsignedInteger('status_id')->index();

This is almost self explanatory. We use the $table object (Blueprint) to define our table structure.

Part Description
$table->increments('id'); Used to create a autoincrement field named id
$table->integer('number'); Used to create an int field name number
$table->string('name'); Used to create a varchar field named name
$table->string('email'); Used to create a varchar field named email, but this time we tell that the size should be only 50
$table->unsignedInteger('status_id')->index(); Used to create a unsigned int names status_id and add an index to it

There are a lot of more options, you should check the Laravel manual online right Here


$table->foreign('status_id')->references('id')->on('t00_boolean_statuses');

This is a relation, we are telling laravel that the field status_id is Foreign Key and is connected to another table

Part Description
$table->foreign('status_id') here we are identifying the FK wich is status_id
->references('id') here we tell the name of the field that ao FK references
->on('t00_boolean_statuses') the table of the referenced key

Basically we are telling that the status_id field is connected to the id field on the t00_boolean_statuses table


Schema::drop('t00_contacts');

This is fairly easy. When we rollback the changes it will drop the table t00_contacts


Seeding the database

Seeds are a way to populate your tables with test or even default production data. Altough this is not a requirement, its highly advisible to create seeds with test data on every table so it's easier to test.

  1. Lets create a seed file

    php artisan make:seeder ContactsTableSeeder

    The seed file will be create in the database/seeds folder with the name ContactsTableSeeder.php

  2. Now lets open the file and add some data. The seed file by default contains only one method, the run method wich is executed with an artisan command

    public function run()
    {
        //dummy data
        $data = [
            [ // row #0
                'number' => 001,
                'name'   => 'Jon Smith',
                'email'  => 'jon.smith@wemake.pt'
            ],
            [ // row #1
                'number' => 007,
                'name'   => 'James Bond',
                'email'  => 'james.bond@wemake.pt'
            ],
        ];
    
        //insert data in table contacts
        DB::table('t00_contacts')->insert($data);
    }
  3. Last, just run the seed

    php artisan db:seed or php artisan db:seed --class=ContactsTableSeeder


Lets Break it down

$data = [
    [ // row #0
        'number' => 001,
        'name'   => 'Jon Smith',
        'email'  => 'jon.smith@wemake.pt'
    ],
    [ // row #1
        'number' => 007,
        'name'   => 'James Bond',
        'email'  => 'james.bond@wemake.pt'
    ],
];

This is just an multidimenstional array where we fill with the test data we want to insert.

DB::table('t00_contacts')->insert($data);
Parts Description
DB Class that runs querys
table('t00_contacts') Table method with a parameter identifying the table we want to use t00_contacts
->insert($data) Method to insert the passed $data array

Model

Each Model corresponds to our tables in the database. This is used to work with Laravel's ORM Eloquent.

Lets create our model. Create the file app/Models/Administration/Contacts.php with the following content

<?php

namespace WeMake\Models\Administration;

use Illuminate\Database\Eloquent\Model;
use WeMake\Models\RegistersEvents;
use WeMake\Repositories\Administration\ModulesRepository;
use WeMake\Repositories\ModelRepository;

/**
 * Class Module
 *
 * @package WeMake\Models\Administration
 */
class Module extends Model
{
    use RegistersEvents;

    /**
     * The table associated with the model.
     *
     * @var string
     */
    protected $table = 't00_contacts';

    /**
     * The attributes that aren't mass assignable.
     *
     * @var array
     */
    protected $guarded = ['id'];

    /**
     * The attributes that are mass assignable.
     *
     * @var array
     */
    protected $fillable = ['name', 'number', 'email', 'status_id'];

    /**
     * Indicates if the model should be timestamped.
     *
     * @var bool
     */
    public $timestamps = true;

    /**
     * Returns an instance of proper model repository
     *
     * @return ModelRepository
     */
    protected function repository()
    {
        return new ModulesRepository;
    }

    /**
     * Returns status
     *
     * @return \Illuminate\Database\Eloquent\Relations\BelongsTo
     */
    public function status()
    {
        return $this->belongsTo(BooleanStatus::class, 'status_id', 'id');
    }
}

Lets Break it down Properties

Property Description
$table This property identifies our table name in the database
$guarded This property is used to tell witch fields should not be able to be massively-assigned
$fillable This is the reverse of $guarded, it allow us to define wich fields can be massively-assigned
$timestams This is used to indicate if the model should be timestamped
protected function repository()
{
    return new ModulesRepository;
}

The method return an instance of the model repository. We will talk about repositories next.

public function status()
{
    return $this->belongsTo(BooleanStatus::class, 'status_id', 'id');
}
TODO: Explain better

This method returns the status


Repository

The repository is used to mediate between the domain and data mapping layer.

The idea is to separate the data access logic with the business logic.

So lets create our table repository. Create the file app/Repositories/Administration/ContactsRepository.php with te following content:

<?php
namespace WeMake\Repositories\Administration;

use Cache;
use Illuminate\Database\Eloquent\Builder;
use WeMake\Models\Administration\BooleanStatus;
use WeMake\Models\Administration\Module;
use WeMake\Repositories\BaseModelRepository;

/**
 * Class ContactsRepository
 *
 * @package WeMake\Repositories
 */
class ModulesRepository extends BaseModelRepository
{
    /**
     * Allowed record names
     *
     * @var array
     */
    protected $allowed = [
        'active_concats',
        'all_concats',
    ];

    /**
     * Returns base query
     *
     * @return Builder
     */
    protected function baseQuery()
    {
        return Contacts::orderBy('t00_contacts.name', 'asc');
    }

    /**
     * Return active records
     *
     * @return mixed
     */
    public function active()
    {
        return Cache::rememberForever($this->parseKey('active_contacts'), function () {
            return $this->baseQuery()
                        ->where('t00_contacts.status_id', '=', BooleanStatus::ACTIVE)
                        ->get();
        });
    }

    /**
     * Return all records
     *
     * @return mixed
     */
    public function all()
    {
        return Cache::rememberForever($this->parseKey('all_contacts'), function () {
            return $this->baseQuery()->get();
        });
    }
}

Routes

Route are the way to tell the software where to go when it receives a Request. So everytime we whant the software to return something we nee to create a route.

Lets create the routes needed to ours requestes The routes file is located in the app/Http/Routes/Api/ folder. Each module has its own file. Open the administration.php file and add the following routes:

    Route::post('concats', ['as' => 'api.administration.concats.store', 'uses' => 'ContactsController@store']);
    Route::put('concats/{concat}', ['as' => 'api.administration.concats.update', 'uses' => 'ContactsController@update']);
    Route::get('concats/{concat}', ['as' => 'api.administration.concats.show', 'uses' => 'ContactsController@show']);
    Route::delete('concats/{concat}/destroy', ['as' => 'api.administration.concats.destroy', 'uses' => 'ContactsController@destroy']);
    Route::post('concats/search', ['as' => 'api.administration.concats.search', 'uses' => 'ContactsController@search']);
    Route::get('concats/all', ['as' => 'api.administration.concats.all', 'uses' => 'ContactsController@all']);
    Route::get('concats/active', ['as' => 'api.administration.concats.active', 'uses' => 'ContactsController@active']);

Breakdown time!

Check this awesome route!!!

    Route::post('concats', ['as' => 'api.administration.concats.store', 'uses' => 'ContactsController@store']);

What does it mean?!?!

Part Description
Route Route is the classe the handles the routes
post This is our verb. This route will only works when the requests are made by post
'contacts' this is the parte URL that it must mach. Something like http://localhost/contacts
'api.administration.concats.store' This is the name of the route
'uses' => 'ContactsController@store' This tells que route que will execute when we match the request. ContactsController is our controller class, store is the method that will be executed.

Controller

Now that we have our routes, we need to create the controller that will receive the request and execute whatever we tell it to do.

Create the file app/Http/Controllers/Administration/ContactsController.php with the following content:

<?php

namespace WeMake\Http\Controllers\Administration;

use Response;
use WeMake\Http\Controllers\RestController;
use WeMake\Http\Requests\Administration\Modules\DestroyRequest;
use WeMake\Http\Requests\Administration\Modules\GetActiveRequest;
use WeMake\Http\Requests\Administration\Modules\GetActiveWithPermissionsRequest;
use WeMake\Http\Requests\Administration\Modules\GetAllRequest;
use WeMake\Http\Requests\Administration\Modules\GetAllWithPermissionsRequest;
use WeMake\Http\Requests\Administration\Modules\SearchRequest;
use WeMake\Http\Requests\Administration\Modules\ShowRequest;
use WeMake\Http\Requests\Administration\Modules\StoreRequest;
use WeMake\Http\Requests\Administration\Modules\UpdateRequest;
use WeMake\Models\Administration\Module;
use WeMake\Repositories\Administration\ModulesRepository as Repository;
use WeMake\Services\Administration\ModulesSearchService as SearchService;

/**
 * Class ModulesController
 *
 * @package WeMake\Http\Controllers\Administration
 */
class ModulesController extends RestController
{
    /**
     * Returns all users
     *
     * @param GetAllRequest $request
     * @param Repository    $repository
     *
     * @return \Illuminate\Http\JsonResponse
     */
    public function all(GetAllRequest $request, Repository $repository)
    {
        $modules = $repository->all();

        return Response::json($modules);
    }

    /**
     * Returns active users
     *
     * @param GetActiveRequest $request
     * @param Repository       $repository
     *
     * @return \Illuminate\Http\JsonResponse
     */
    public function active(GetActiveRequest $request, Repository $repository)
    {
        $modules = $repository->active();

        return Response::json($modules);
    }

    /**
     * Display a filtered listing of the resource.
     *
     * @param SearchRequest $request
     * @param SearchService $searchService
     *
     * @return \Illuminate\Http\JsonResponse
     */
    public function search(SearchRequest $request, SearchService $searchService)
    {
        return $searchService->search($request);
    }

    /**
     * Store a newly created resource in storage.
     *
     * @param StoreRequest $request
     * @param Module       $module
     *
     * @return \Illuminate\Http\JsonResponse
     */
    public function store(StoreRequest $request, Module $module)
    {
        $operation = function () use ($request, $module) {
            $module->fill($request->clean())->save();
        };

        $operationResult = $this->transactionService->run($operation);

        return $this->responseHandler->defaultStoreResponse($operationResult->status());
    }

    /**
     * Display the specified resource.
     *
     * @param ShowRequest $request
     * @param Module      $module
     *
     * @return \Illuminate\Http\JsonResponse
     */
    public function show(ShowRequest $request, Module $module)
    {
        return Response::json($module);
    }

    /**
     * Update the specified resource in storage.
     *
     * @param UpdateRequest $request
     * @param Module        $module
     *
     * @return \Illuminate\Http\JsonResponse
     */
    public function update(UpdateRequest $request, Module $module)
    {
        $operation = function () use ($request, $module) {
            $module->fill($request->clean())->save();
        };

        $operationResult = $this->transactionService->run($operation);

        return $this->responseHandler->defaultUpdateResponse($operationResult->status());
    }

    /**
     * Remove the specified resource from storage.
     *
     * @param DestroyRequest $request
     * @param Module         $module
     *
     * @return \Illuminate\Http\JsonResponse
     */
    public function destroy(DestroyRequest $request, Module $module)
    {
        $operation = function () use ($request, $module) {
            $module->delete();
        };

        $operationResult = $this->transactionService->run($operation);

        return $this->responseHandler->defaultDestroyResponse($operationResult->status());
    }
}

Breakdown time! Ok, theres a lot of stuff in here that we didn't create

Requests

2. Frontend

Route

  1. Lets add some routes to our frontend

    Open the file src/common/configs/router-config.js and add the following routes:

    First, the route to present the listing

{
    route:    'administration/contacts',
        name
:
    'administration.contacts.index',
        moduleId
:
    './modules/administration/contacts/index',
        title
:
    'Contacts List',
        nav
:
    true,
}

Then the route for the contact creation

{
    route:    'administration/contacts/create',
        name
:
    'administration.contacts.create',
        moduleId
:
    './modules/administration/contacts/create/index',
        title
:
    'Create Contact',
        nav
:
    true,
}

Finally the route for contact editing

{
    route:    'administration/contacts/:id/edit',
        name
:
    'administration.contacts.edit',
        moduleId
:
    './modules/administration/contacts/edit/index',
        title
:
    'Edir Contact',
        nav
:
    true,
}

Lets just breakdown the routes

param description
route This is going to me our URL, something like http://localhost/administration/management/contacts
name This is going to be the routes name, it cam be used later for navigation
moduleId This is the path in our source code to the folder and file thate going to load when the route is matched
title This is just the title of our route.
nav this just tells if this route is going to be included in the router's navigation model.

View and ViewModel

The View and the ViewModel should me placed on the respective folder according to our defined route.

  1. So first lets create the listing View and ViewModel For the View create a file named index.html on the src/modules/administration/management/contacts/ with the following content:

<template>
    <panel-group header-title.bind="headerTitle & t" header-class="bg-teal-300">
        <datatable schema.bind="datatable" listing-id.bind="listingId"></datatable>
    </panel-group>
    <new-record route.bind="newRecordRoute"></new-record>
</template>

For the ViewModel create a file name index.js on the same folder with the following content:

import { bindable, inject }  from "aurelia-framework";
import { AppContainer }      from "wemake-services/app-container";
import { BaseViewModel }     from "base-view-model";
import { ModulesRepository } from "./services/repository";

@inject(AppContainer, ModulesRepository)
export class ListModules extends BaseViewModel {

    listingId = 'administration-contacts-listing';

    @bindable headerTitle    = 'form.title.listing';
    @bindable newRecordRoute = 'administration.contacts.create';
    @bindable repository;
    @bindable datatable;

    /**
     * Constructor
     *
     * @param appContainer
     * @param repository
     */
    constructor(appContainer, repository) {
        super(appContainer);

        this.repository = repository;
    }

    /**
     * Handles activate event
     */
    activate() {
        super.activate();

        this.defineDatatable();
    }

    /**
     * Handles attached event
     */
    attached() {
        super.attached();
    }

    /**
     * Defines table columns
     */
    defineDatatable() {
        this.datatable = {
            repository: this.repository,
            edit:       'administration.contacts.edit',
            actions:    [],
            buttons:    [],
            selectable: true,
            sorting:    {
                column:    0,
                direction: 'asc'
            },
            columns:    [
                {
                    data:  'id',
                    name:  'contacts.id',
                    title: '#'
                },
                {
                    data:  'number',
                    name:  'contacts.number',
                    title: 'form.field.contacts'
                },
                {
                    data:  'name',
                    name:  'contacts.name',
                    title: 'form.field.name'
                },
                {
                    data:  'email',
                    name:  'contacts.email',
                    title: 'form.field.email',
                }
            ]
        }
    }
}

Repository