visit
This article originally appeared on .
In this tutorial, we are going to be creating a simple inventory management application with Laravel and Vue.js as our frontend. This tutorial assumes you have a basic knowledge of object oriented php and javascript, and though we will be going through the basics of Laravel and Vue.js, it is recommended to have a basic understanding of their concepts. Now that we have that cleared, fire up your php server and let’s build something.composer create-project --prefer-dist laravel/laravel inventory
We will be using some functions from the Cosmic JS php library, download the repo into a separate folder, we will have to edit it a bit to work neatly with laravel. In the
inventory/app/
folder, create a /Vendor/cosmicjs
folder and copy all the contents of the cosmicjs-php library into it, such that for example the path for cosmicjs.php
becoms app/Vendor/cosmicjs/cosmicjs.php
. Then rename app/Vendor/cosmicjs/curl
class to app/Vendor/cosmicjs/cosmiccurl
and change this top part of the code:class Curl {
...
}
namespace App\Vendor\cosmicjs;
class CosmicCurl {
....
}
What we did was add a namespace to the cosmiccurl file so we can import into laravel and change the class name to match the file name. After doing that replace this section of cosmicjs.php
include("curl.php");
$curl = new Curl;
class CosmicJS {
function __construct(){
global $curl;
global $config;
$this->curl = $curl;
$this->config = $config;
$this->config->bucket_slug = $config->bucket_slug;
$this->config->object_slug = $config->object_slug;
$this->config->read_key = $config->read_key;
$this->config->write_key = $config->write_key;
$this->config->url = "//api.cosmicjs.com/v1/" . $this->config->bucket_slug;
$this->config->objects_url = $this->config->url . "/objects?read_key=" . $this->config->read_key;
$this->config->object_url = $this->config->url . "/object/" . $this->config->object_slug . "?read_key=" . $this->config->read_key;
$this->config->media_url = $this->config->url . "/media?read_key=" . $this->config->read_key;
$this->config->add_object_url = $this->config->url . "/add-object?write_key=" . $this->config->write_key;
$this->config->edit_object_url = $this->config->url . "/edit-object?write_key=" . $this->config->write_key;
$this->config->delete_object_url = $this->config->url . "/delete-object?write_key=" . $this->config->write_key;
}
namespace App\Vendor\cosmicjs;
use App\Vendor\cosmicjs\CosmicCurl;
class CosmicJS {
private $config;
private $curl;
function __construct($bucket_slug, $type_slug,$object_slug = "", $read_key = "", $write_key = "") {
$this->curl = new CosmicCurl();
$this->config = new \stdClass();
//$this->config = $config;
$this->config->bucket_slug = $bucket_slug;
$this->config->object_slug = $object_slug;
$this->config->type_slug = $type_slug;
$this->config->read_key = $read_key;
$this->config->write_key = $write_key;
$this->config->url = "//api.cosmicjs.com/v1/" . $this->config->bucket_slug;
$this->config->objects_url = $this->config->url . "/objects?read_key=" . $this->config->read_key;
$this->config->object_type_url = $this->config->url . "/object-type/" . $this->config->type_slug . "?read_key=" . $this->config->read_key;
$this->config->object_url = $this->config->url . "/object/" . $this->config->object_slug . "?read_key=" . $this->config->read_key;
$this->config->media_url = $this->config->url . "/media?read_key=" . $this->config->read_key;
$this->config->add_object_url = $this->config->url . "/add-object?write_key=" . $this->config->write_key;
$this->config->edit_object_url = $this->config->url . "/edit-object?write_key=" . $this->config->write_key;
$this->config->delete_object_url = $this->config->url . "/delete-object?write_key=" . $this->config->write_key;
}
public function getByObjectSlug($key,$slug)
{
$this->config->object_by_meta_object = $this->config->url ."/object-type/" . $this->config->type_slug ."/search?metafield_key=" . $key ."&metafield_object_slug=" .$slug;
$data = json_decode($this->curl->get($this->config->object_by_meta_object));
return $data;
}
Now that we have our cosmicjs library setup in the app/Vendor folder, its time to actually build something. Since all requests will be handled by the
app/Http/Controller/IndexController.php
file open it up and copy and paste this code into it.<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use App\Vendor\cosmicjs\CosmicJS;
use GuzzleHttp\Client;
class IndexController extends Controller {
private $locations_cosmic;
private $items_cosmic;
private $bucket_slug = '';
private $read_key = '';
private $write_key = '';
public function __construct() {
//initialize cosmicjs php instance for fetching all locations
$this->bucket_slug = config('cosmic.slug');
$this->read_key = config('cosmic.read');
$this->write_key = config('cosmic.write');
$this->locations_cosmic = new CosmicJS($this->bucket_slug, 'locations');
$this->items_cosmic = new CosmicJS($this->bucket_slug, 'items', $this->read_key, $this->write_key);
}
public function index($location = null) {
//get objects with cosmic-js php
$locations = $this->locations_cosmic->getObjectsType();
//set locations and bucket_slug variable to be passed to view
if (property_exists($locations, 'objects')) {
$data['locations'] = $locations->objects;
}
else
{
$data['locations'] = [];
}
$data['bucket_slug'] = $this->bucket_slug;
//if location slug was passed in url, pass it to view as well
if ($location) {
$data['location_slug'] = $location;
} else {
$data['location_slug'] = '';
}
//load view
return view('index', $data);
}
//fetch items for location based on slug
public function itemsByLocation($slug) {
//fetch items using the cosmicjs library's custom function
$items = $this->items_cosmic->getByObjectSlug('location', $slug);
//if the returned value has "object" property, pass it
if (property_exists($items, 'objects')) {
//returning arrays in laravel automatically converts it to json string
return $items->objects;
} else {
return 0;
}
}
public function newLocation(Request $request) {
//get passed input
$title = $request->input('title');
$address = $request->input('address');
$picture = $request->input('image');
//set data array
$data['title'] = $title;
$data['type_slug'] = "locations";
$data['bucket_slug'] = $this->bucket_slug;
$metafields = array();
$address_data['key'] = "address";
$address_data['type'] = 'textarea';
$address_data['value'] = $address;
if ($picture != '') {
$picture_data['key'] = "picture";
$picture_data['type'] = 'file';
$picture_data['value'] = $picture;
array_push($metafields, $picture_data);
}
array_push($metafields, $address_data);
$data['metafields'] = $metafields;
//create a new guzzle client
$client = new Client();
//create guzzle request with data array passed as json value
$result = $client->post('//api.cosmicjs.com/v1/' . $this->bucket_slug . '/add-object', [
'json' => $data,
'headers' => [
'Content-type' => 'application/json',
]
]);
//flash message
$request->session()->flash('status', 'The location"' . $title . '" was successfully locations');
//return result body
return $result->getBody();
}
//create a new item
public function newItem(Request $request) {
//get data
$name = $request->input('name');
$count = $request->input('count');
$location_id = $request->input('location');
$picture = $request->input('image');
//create data array to be passed
$data['title'] = $name;
$data['type_slug'] = "items";
$data['bucket_slug'] = $this->bucket_slug;
$count_metafield['key'] = "count";
$count_metafield['value'] = $count;
$count_metafield['type'] = "text";
$location_meta['key'] = "location";
$location_meta['object_type'] = "locations";
$location_meta['type'] = "object";
$location_meta['value'] = $location_id;
$metafields = array();
//set picture if passed into request
if ($picture != '') {
$picture_data['key'] = "picture";
$picture_data['type'] = 'file';
$picture_data['value'] = $picture;
array_push($metafields, $picture_data);
}
array_push($metafields, $count_metafield);
array_push($metafields, $location_meta);
$data['metafields'] = $metafields;
$client = new Client();
$result = $client->post('//api.cosmicjs.com/v1/' . $this->bucket_slug . '/add-object', [
'json' => $data,
'headers' => [
'Content-type' => 'application/json',
]
]);
//flash message
$request->session()->flash('status', 'The Item "' . $name . '" was successfully created');
//return result body
return $result->getBody();
}
public function editItem(Request $request) {
$name = $request->input('name');
$count = $request->input('count');
$slug = $request->input('slug');
$location_id = $request->input('location_id');
$data['title'] = $name;
$data['slug'] = $slug;
$count_meta['key'] = "count";
$count_meta['value'] = $count;
$count_meta['type'] = "text";
$location_meta['key'] = "location";
$location_meta['object_type'] = "locations";
$location_meta['type'] = "object";
$location_meta['value'] = $location_id;
$metafields = array();
//set picture if passed into request
if ($request->input('image')) {
$picture_data['key'] = "picture";
$picture_data['type'] = 'file';
$picture_data['value'] = $request->input('image');
array_push($metafields, $picture_data);
}
array_push($metafields, $count_meta);
array_push($metafields, $location_meta);
$data['metafields'] = $metafields;
$client = new Client();
$result = $client->put('//api.cosmicjs.com/v1/' . $this->bucket_slug . '/edit-object', [
'json' => $data,
'headers' => [
'Content-type' => 'application/json',
]
]);
//flash message
$request->session()->flash('status', 'The Item was successfully edited!');
//return result body
return $result->getBody();
}
public function deleteItem(Request $request, $slug) {
//create new client and delete item
$client = new Client();
$result = $client->delete('//api.cosmicjs.com/v1/' . $this->bucket_slug . '/' . $slug, [
'headers' => [
'Content-type' => 'application/json',
]
]);
//flash message
$request->session()->flash('status', 'The Item was successfully deleted!');
return $result;
}
}
Next we will create our routes in the
routes/web.php
file. Open up the file and copy and paste this code into it.<?php
Route::get('/{location?}', 'IndexController@index');
Route::get('items/{slug}', 'IndexController@itemsByLocation');
Route::post('locations/new','IndexController@newLocation');
Route::post('items/new','IndexController@newItem');
Route::post('items/edit','IndexController@editItem');
Remember this code in our IndexController return view(‘index’, $data);? well its time to create the view that will be loaded. Open up the
/resources/views
folder and open up the then copy and paste this into it.<html lang="{{ config('app.locale') }}">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1">
<!-- Set Csrf token on all pages -->
<meta name="csrf-token" content="{{ csrf_token() }}">
<!-- Load Bootstrap-->
<link rel="stylesheet" href="//maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css" integrity="sha384-BVYiiSIFeK1dGmJRAkycuHAHRg32OmUcww7on3RYdg4Va+PmSTsz/K68vbdEjh4u" crossorigin="anonymous">
<link href="//cdnjs.cloudflare.com/ajax/libs/limonte-sweetalert2/6.6.2/sweetalert2.css" rel="stylesheet" type="text/css">
<title>Inventory Manger</title>
<!-- Fonts -->
<link rel="stylesheet" href="{{ asset('css/font-awesome/css/font-awesome.min.css')}}"/>
<link href="//fonts.googleapis.com/css?family=Raleway:100,600" rel="stylesheet" type="text/css">
<script src="//use.fontawesome.com/682442a8be.js"></script>
<!-- Set Csrf token to be used by javascript and axios-->
<script>
window.Laravel = <?php
echo json_encode([
'csrfToken' => csrf_token(),
]);
?>
</script>
<!-- Styles -->
<style>
.location-tab{
height:104px;
padding-left: 150px;
}
.location-tab > img{
position: absolute;
left: 0;
top: 0;
height: 100%;
width: auto;
max-width: 130px;
}
.text-primary{
color: #29ABE2 !important;
}
.panel-heading{
background-color: #29ABE2 !important;
color: white !important;
}
.panel{
border-color: #29ABE2 !important;
}
.btn-primary{
background-color: #29ABE2 !important;
color: white !important;
border-color: #29ABE2 !important;
border-radius: 3px;
margin: 10px 0;
}
</style>
</head>
<body>
<div class="container">
<div id="wrapper">
@yield('content')
</div>
</div>
<!-- Load Jquery and bootstrap js-->
<script src="//code.jquery.com/jquery-3.2.1.min.js" integrity="sha256-hwg4gsxgFZhOsEEamdOYGBf13FyQuiTwlAQgxVSNgt4=" crossorigin="anonymous"></script>
<script src="//maxcdn.bootstrapcdn.com/bootstrap/3.3.7/js/bootstrap.min.js" integrity="sha384-Tc5IQib027qvyjSMfHjOMaLkfuWVxZxUPnCJA7l2mCWNIpG9mGCD8wGNIcPD7Txa" crossorigin="anonymous"></script>
<script src="//cdnjs.cloudflare.com/ajax/libs/limonte-sweetalert2/6.6.2/sweetalert2.min.js"></script>
<script src="{{ asset('/js/app.js')}}"></script>
@yield('scripts')
</body>
</html>
@extends('master')
@section('content')
<div class="row">
<div class="col-md-12">
<div style="float:left">
<h1>Inventory Management</h1>
</div>
<div style="float:right;padding-top: 20px">
<a class="btn btn-default"><i class="fa fa-github"></i> View on Github</a>
</div>
</div>
</div>
<div class="row">
<div class="col-md-12">
<div style="float: right; margin-bottom: 15px;"><a href="//cosmicjs.com" target="_blank" style="text-decoration: none;"><img class="pull-left" src="//cosmicjs.com/images/logo.svg" width="28" height="28" style="margin-right: 10px;"><span style="color: rgb(102, 102, 102); position: relative; top: 3px;">Proudly powered by Cosmic JS</span></a></div>
</div>
</div>
<div class="row" style="font-size: 16px">
<!-- Display vue component and set props from given data -->
<inventory message="{{Session::get('status')}}" :initial-locations="{{ json_encode($locations) }}" slug="{{ $bucket_slug }}" location-slug="{{ $location_slug }}"></inventory>
</div>
@endsection
@section('scripts')
<script>
</script>
@endsection
This section assumes you have fundamental knowledge of Vuejs, if not i recommend you brush up on it, as explaining how some vue functions works is out of the scope of this tutorial. Now to begin, open a command prompt and cd to the app’s folder then run npm run watch to fire up laravel mix, this will compile our assets whenever a change has been made to any of our files, alternatively you could type npm run dev whenever you need to compile the assets yourself. Open the
/resources/assets/js/app.js
file and change thisVue.component('example', require('./components/Example.vue'));
Vue.component('inventory', require('./components/Inventory.vue'));
Here we are replacing the default example component with a component called inventory which we will be the
/resources/assets/js/components
folder create and file to house our component. In the newly created file copy and paste this code into it<template>
<div>
<!---- ADD LOCATION FORM -->
<div v-if="add_location">
<button class="btn btn-primary" v-on:click="add_location=false"><span class="glyphicon glyphicon-chevron-left" aria-hidden="true"></span> Go back</button>
<div class="panel panel-default">
<div class="panel-heading">Add New Location</div>
<div class="panel-body">
<form id="location_form" name="location">
<div class="form-group">
<label for="name">Name</label>
<input type="text" class="form-control" name="title" required="">
<label for="address">Address</label>
<input type="text" class="form-control" name="address" required="">
<label for="image">Image</label>
<input type="file" class="form-control media" name="media"/>
</div>
<button type="submit" class="btn btn-primary" :class="{disabled: isDisabled}" v-on:click.prevent="addLocation">Submit</button>
</form>
</div>
</div>
</div>
<div v-else>
<!---- LOCATIONS LIST -->
<div v-if="unselected">
<button class="btn btn-primary pull-right" v-on:click="add_location = true"><span class="glyphicon glyphicon-plus" aria-hidden="true"></span>Add New</button>
<ul class="list-group">
<button type="button" class="list-group-item location-tab text-primary" :class="{disabled: list_disable}" v-for="location in locations" v-on:click="fetchItems(location)"><img v-if="location.metadata.hasOwnProperty('picture')" :src="location.metadata.picture.url">{{ location.title }} - {{ location.metadata.address}}</button>
</ul>
</div>
<div v-else>
<!---- ADD ITEM FORM -->
<div v-if="add_item">
<button class="btn btn-primary" v-on:click="add_item=false"><span class="glyphicon glyphicon-chevron-left" aria-hidden="true"></span> Go back</button>
<div class="panel panel-default">
<div class="panel-heading">Add New Item</div>
<div class="panel-body">
<form id="item_form">
<div class="form-group">
<label for="name">Name</label>
<input type="text" class="form-control" name="name">
</div>
<div class="form-group">
<label for="count">Count</label>
<input type="number" class="form-control" name="count">
</div>
<div>
<label for="image">Image</label>
<input type="file" class="form-control media" name="media"/>
</div>
<button type="submit" class="btn btn-primary" :class="{disabled: isDisabled}" v-on:click.prevent="addItem">Submit</button>
</form>
</div>
</div>
</div>
<!---- EDIT ITEM FORM -->
<div v-else-if="edit_item">
<button class="btn btn-primary" v-on:click="edit_item=false"><span class="glyphicon glyphicon-chevron-left" aria-hidden="true"></span> Go back</button>
<div class="panel panel-default">
<div class="panel-heading">Edit {{ selected_item.title }}</div>
<div class="panel-body">
<form id="edit_item">
<div class="form-group">
<label for="name">Name</label>
<input type="text" class="form-control" name="name" :value="selected_item.title">
</div>
<div class="form-group">
<label for="count">Count</label>
<input type="number" class="form-control" name="count" :value="selected_item.metadata.count">
</div>
<button type="submit" class="btn btn-primary" :class="{disabled: isDisabled}" v-on:click.prevent="editItem">Submit</button>
</form>
</div>
</div>
</div>
<div v-else>
<!---- ITEMS LIST -->
<button class="btn btn-primary" v-on:click="unselected=true"><span class="glyphicon glyphicon-chevron-left" aria-hidden="true"></span> Go back</button>
<button class="btn btn-primary pull-right" v-on:click="add_item = true"><span class="glyphicon glyphicon-plus" aria-hidden="true"></span>Add New Item</button>
<div class="panel panel-default">
<div class="panel-heading">{{ selected_location.title }}</div>
<div class="panel-body">
<ul class="list-group">
<button type="button" class="list-group-item text-primary location-tab" :class="{disabled: isDisabled}" v-for="item in items"><img v-if="item.metadata.hasOwnProperty('picture')" :src="item.metadata.picture.url">{{ item.title }} - {{ item.metadata.count }} <div class="pull-right"><span class="glyphicon glyphicon-pencil" aria-hidden="true" v-on:click.prevent="openEdit(item)"></span><span class="glyphicon glyphicon-trash" aria-hidden="true" v-on:click.prevent="deleteItem(item)" style="padding: 0 5px;"></span></div></button>
</ul>
</div>
</div>
</div>
</div>
</div>
</div>
</template>
<script>
export default {
mounted() {
var self = this;
//If location slug was passed show items for that location
if(this.message)
{
swal(this.message);
}
if (this.locationSlug)
{
this.unselected = false;
//find location with slug
var item = this.locations.filter(function (obj)
{
return obj.slug === self.locationSlug;
});
this.selected_location = item[0];
this.fetchItems(this.selected_location);
}
},
props: ['initial-locations', 'slug', 'location-slug','message'],
data: function () {
return {
edit_item: false,
locations: this.initialLocations,
isDisabled: false,
list_disable: false,
unselected: true,
items: [],
add_location: false,
selected_location: [],
selected_item: [],
add_item: false
};
},
methods: {
fetchItems(location)
{
//disable the list and fetch items from laravel
var self = this;
this.list_disable = true;
axios.get('items/' + location.slug).then(response => {
if (response.data.constructor === Array)
{
self.items = (response.data);
self.selected_location = location;
self.unselected = false;
} else {
self.selected_location = location;
self.items = [];
self.unselected = false;
}
self.list_disable = false;
});
},
addLocation()
{
//disable button
this.isDisabled = true;
var image = '';
var form = $("#location_form")[0];
var data = new FormData(form);
//Check if image is selected then upload image first
if ($("#location_form .media").val() !== '')
{
//delete X-csrf-token default header as it is not accepted by cosmic api
delete axios.defaults.headers.common["X-CSRF-TOKEN"];
axios.post('//api.cosmicjs.com/v1/' + this.slug + '/media', data).then(function (response)
{
//set x-csrf-token again
window.axios.defaults.headers.common['X-CSRF-TOKEN'] = window.Laravel.csrfToken;
//get image name, append to formdata and send form data to laravel to add location
image = response.data.media.name;
data.set('image', image);
axios.post('locations/new', data).then(response => {
location.reload(true);
});
});
} else {
window.axios.defaults.headers.common['X-CSRF-TOKEN'] = window.Laravel.csrfToken;
//send form data to laravel without image
axios.post('locations/new', data).then(response => {
location.reload(true);
});
}
},
//set selected item and open edit item section
openEdit(item)
{
this.selected_item = item;
this.edit_item = true;
},
addItem() {
var self = this;
this.isDisabled = true;
var form = $('#item_form')[0];
var data = new FormData(form);
data.append('location', this.selected_location._id);
//Check if image is selected the upload image first
if ($("#item_form .media").val() !== '')
{
//delete X-csrf-token default header as it is not allowed by cosmic api and post
delete axios.defaults.headers.common["X-CSRF-TOKEN"];
axios.post('//api.cosmicjs.com/v1/' + this.slug + '/media', data).then(function (response)
{
//set x-csrf-token again
window.axios.defaults.headers.common['X-CSRF-TOKEN'] = window.Laravel.csrfToken;
//get image name, append to formdata and send form data to laravel to add location
var image = response.data.media.name;
data.set('image', image);
axios.post('items/new', data).then(response => {
//refresh page BUT pass location_slug, which then makes the app load into the passed location
window.location.href = "./" + self.selected_location.slug;
});
});
} else {
//add header back after post
window.axios.defaults.headers.common['X-CSRF-TOKEN'] = window.Laravel.csrfToken;
//send form data to laravel without image
axios.post('items/new', data).then(response => {
window.location.href = "./" + self.selected_location.slug;
});
}
},
editItem()
{
//edit item, by sending data to IndexController's editItem() function
var self = this;
var form = $("#edit_item")[0];
var data = new FormData(form);
this.isDisabled = true;
data.append('slug', this.selected_item.slug);
if(this.selected_item.metadata.hasOwnProperty('picture')){
data.append('image',this.selected_item.metafields[0].value);
}
data.append('location_id', this.selected_location._id);
axios.post('items/edit', data).then(response => {
//refresh page BUT pass location_slug, which then makes the app load into the passed location
window.location.href = "./" + self.selected_location.slug;
});
},
deleteItem(item)
{
var self = this;
swal({
title: 'Are you sure?',
text: 'You will not be able to recover this item!',
type: 'warning',
showCancelButton: true,
confirmButtonText: 'Yes, delete it!',
cancelButtonText: 'Nope, still need it'
}).then(function(){
axios.get('item/' + item.slug + '/delete').then(response => {
window.location.href = "./" + self.selected_location.slug;
});
})
}
}
}
</script>
Now we want to set our bucket slug. We want to be able to set it by running an artisan comman so run
php artisan make:command SetSlug
. Now navigate to app/console/Commands/SetSlug
and copy and paste this into it:<?php
namespace App\Console\Commands;
use Illuminate\Console\Command;
class SetSlug extends Command {
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'bucket {slug} {read?} {write?}';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Set the bucket slug';
/**
* Create a new command instance.
*
* @return void
*/
protected $files;
protected $read;
protected $write;
public function __construct(\Illuminate\Filesystem\Filesystem $files) {
parent::__construct();
$this->files = $files;
}
/**
* Execute the console command.
*
* @return mixed
*/
public function handle() {
$this->read = $this->argument('slug');
$this->write = $this->argument('slug');
$config_path = base_path() . "/config/cosmic.php";
$content = "<?php\n\treturn [\n\t\t'slug' => '" . $this->argument('slug') . "',\n\t\t"
. "'read' => '" . $this->argument('read')."',\n\t\t"
."'write' => '" . $this->argument('write')."',\n\t];";
$this->files->put($config_path, $content);
echo "Bucket variables set";
}
}
protected $commands = [
Commands\SetSlug::class
];