Recently I was looking for a new job for myself and applied for PHP developer positions in various companies. Some of them gave test tasks to test my skills. I decided to create a new category on the blog with the analysis of test tasks that I came across. I already wrote about one of them in detail in the article “Creating a pizza shop in Laravel 8”.

Today I will analyze the next task that I received.

Working with products in Laravel

The text of the pre-employment skills assessment test task was as follows.

There is a products table with fields: id, name, price. It is necessary to implement its display with the following functionality:

• Pagination of 25 records per page

• Adding a new product

• Editing a product

• Deleting product

Data output and operations must be done without page reloading page using vue.js. The required Product model, factory and migration have already been created. The database used is irrelevant (both mysql and sqlite can be used). Also, the TestController controller has already been created, where you can place the necessary functionality. It is necessary to create a service layer and organize data validation.

Thus, I got a project in Laravel 7 with some presets (migration, factory, seeder, model) for working with products.

Getting Started with Vue Router in Laravel 7 project

To start execution the task, I needed to install Vue scaffolding using the following command:

php artisan ui vue

Of course, before that, the laravel/ui dependency was added to Composer (you can add it like this: composer require laravel/ui:^2.4 ).

You also need to add Vue Router to the project to create frontend routing.

npm install vue-router

After that, I created the resources/js/routes.js file, which imports the components (I will create them a little later) and describes which path will refer to which component.

import Main from './components/Main.vue';
import Add from './components/Add.vue';
import Edit from './components/Edit.vue';

export const routes = [
    {
        path:'/',
        component:Main
    },
    {
        path:'/add',
        component:Add
    },
    {
        path:'/edit/:product_id',
        component:Edit
    }
];

Now in resources/js/app.js you need to connect the VueRouter and the newly created file with paths.

require('./bootstrap');

window.Vue = require('vue');

import VueRouter from 'vue-router';
import BootstrapVue from 'bootstrap-vue';

Vue.use(VueRouter);
Vue.use(BootstrapVue);

import { routes } from './routes';
const router = new VueRouter({
    routes,
    mode: 'hash'
});

const app = new Vue({
    el: '#app',
    router
});

Now we need to make the frontend handle all paths from the browser. To do this, in the routes/web.php file add the line:

Route::get('/{any}', 'TestController@index')->where('any', '.*');

It says that when you go to any path in the browser, there will be a call to the index method in TestController. And this method looks like this:

public function index()
{
    return view('index');
}

It just includes the resources/views/index.blade.php file, which contains the html page, where the next tag is located in the place where the vue content will be located.

<router-view/>

Creating Vue Components for a Laravel Project

Now is the time to create Vue components that we described earlier in the routes.js file. We will need to create 3 components: Main – to display a list of products, Add – to add products, Edit – to edit a product.

All Vue components will be located in the resources/js/components folder.

I’ll start with the Main.vue component.

export default {
        data() {
            return {
                fields: [
                    'id',
                    'name',
                    'price',
                    { key: 'actions', label: 'Actions' }
                ],
                perPage: 25,
                currentPage: 1,
                products: []
            }
        },
        mounted() {
            this.getProducts();
        },
        computed: {
            rows() {
                return this.products.length
            }
        },
        methods: {
            async getProducts() {
                axios.get('/api/products')
                    .then(res => {
                        this.products = res.data.data;
                    }).catch(err => {
                        console.log(err);
                    });
            },
            async deleteProduct(product_id) {
                axios.delete('/api/products/'+product_id).then(response => {
                    if (response.data === true) {
                        this.getProducts();
                        alert('Продукт успешно удален!');
                    } else {
                        alert('Ошибка удаления!');
                    }
                }).catch(err => {
                    console.log(err);
                });
            }
        }
    }

As you can see, inside mounted, the getProducts method is executed to get a list of products via the API from the backend (we will create api routes a little later). The data also has properties responsible for the columns in the table (fields) and pagination of products (perPage, currentPage).

Products are displayed inside the b-table component (if you don’t have one, then install bootstrap-vue and add its support to app.js, as shown above).

<b-table
        id="my-table"
        :fields="fields"
        :items="products"
        :per-page="perPage"
        :current-page="currentPage"
>
    <template #cell(actions)="data">
        <router-link :to="'/edit/'+data.item.id" class="btn btn-link" >Edit</router-link>
        <button class="btn btn-link" @click="deleteProduct(data.item.id)">Delete</button>
    </template>
</b-table>

<p class="mt-3">Page: {{ currentPage }}</p>

<b-pagination
        v-model="currentPage"
        :total-rows="rows"
        :per-page="perPage"
        aria-controls="my-table"
></b-pagination>

The router-link tag is a replacement for the a tag in Vue.js and by clicking on it, the user will be taken to the product edit page without reloading the page. And the Delete button will call the deleteProduct method, which will make a request to the api, delete the selected product, and then update the product list.

Inside the Add.vue component, we will add a new product.

<template>
    <div>
        <div class="row justify-content-center">
            <div class="col-md-8">
                <div class="card">
                    <div class="card-header">Add product</div>

                    <div class="card-body">
                        <div class="form-group row">
                            <label for="name" class="col-sm-2 col-form-label">Name</label>
                            <div class="col-sm-10">
                                <input type="email" class="form-control" id="name" v-model="name" >
                                <strong v-if="errors" >{{errors.name[0]}}</strong>
                            </div>
                        </div>
                        <div class="form-group row">
                            <label for="price" class="col-sm-2 col-form-label">Price</label>
                            <div class="col-sm-10">
                                <input type="number" class="form-control" id="price" v-model="price">
                                <strong v-if="errors" >{{errors.price[0]}}</strong>
                            </div>
                        </div>
                        <div class="form-group row">
                            <div class="col-sm-10">
                                <button type="submit" class="btn btn-primary" @click="addProduct()">Save</button>
                            </div>
                        </div>
                    </div>
                </div>
            </div>
        </div>
    </div>
</template>

<script>
    export default {
        data() {
            return {
                name: '',
                price: 0,
                errors: null
            }
        },
        methods: {
            async addProduct() {
                const params = {
                    name: this.name,
                    price: this.price
                };
                axios.post('/api/products', params).then(response => {
                    if (typeof response.data.errors !== "undefined") {
                        this.errors = response.data.errors;
                    } else {
                        alert('Продукт успешно добавлен');
                        this.$router.push("/");
                    }
                }).catch(err => {
                    console.log(err);
                });
            }
        }
    }
</script>

When you click on the Save button, the addProduct method will be called, which will send the data entered in the name and price fields to the backend. If the product is successfully added, the user will be redirected to the main page, and if validation errors occur, they will be displayed below the fields.

In the last component of the application, Edit.vue, the product will be edited. It is very similar to the add component, but it has a getProduct method that, depending on the product id taken from the url, will receive product data.

export default {
        data() {
            return {
                name: '',
                price: 0,
                errors: null
            }
        },
        mounted() {
            this.getProduct(this.$route.params.product_id);
        },
        methods: {
            async getProduct(product_id) {
                axios.get('/api/products/'+product_id)
                    .then(res => {
                        this.name = res.data.data.name;
                        this.price = res.data.data.price;
                    }).catch(err => {
                        console.log(err);
                    });
            },
            async editProduct() {
                const params = {
                    name: this.name,
                    price: this.price
                };
                axios.put('/api/products/'+this.$route.params.product_id, params).then(response => {
                    if (typeof response.data.errors !== "undefined") {
                        this.errors = response.data.errors;
                    } else {
                        alert('Продукт успешно отредактирован.');
                        this.$router.push("/");
                    }

                }).catch(err => {
                    console.log(err);
                });
            }
        }
    }

The editProduct method will send the edited fields to the backend.

Building an API in Laravel 7 to work with products

The frontend part of the test task project for working with products in Laravel 7 is ready, now we can start creating the backend.

Let’s start by creating API routes, they are created in the routes/api.php file.

Route::get('/products', 'TestController@getProducts')->name('get.products');
Route::get('/products/{product_id}', 'TestController@getProduct')->where('product_id', '[0-9_\-]+')->name('get.product');
Route::post('/products', 'TestController@add')->name('add.product');
Route::put('/products/{product_id}', 'TestController@edit')->where('product_id', '[0-9_\-]+')->name('edit.product');
Route::delete('/products/{product_id}', 'TestController@delete')->where('product_id', '[0-9_\-]+')->name('delete.product');

Here we just implemented the routes that our Vue components accessed earlier. The controller they access contains the following methods.

public function getProducts(ProductService $productService)
{
    return $productService->getProducts();
}

public function getProduct(ProductService $productService, Request $request)
{
    return $productService->getProduct($request);
}

public function add(ProductService $productService, Request $request)
{
    $validator = $productService->validateProduct($request->all());
    if ($validator->fails()) {
        return response()->json(['errors'=>$validator->errors()], 200);
    }
    return $productService->add($request);
}

public function edit(ProductService $productService, Request $request)
{
    $validator = $productService->validateProduct($request->all());
    if ($validator->fails()) {
        return response()->json(['errors'=>$validator->errors()], 200);
    }
    return $productService->edit($request);
}

public function delete(ProductService $productService, Request $request)
{
    return response()->json($productService->delete($request), 200);
}

The methods implement getting products, getting a single product, adding, editing and deleting products. As you can see, the ProductService (where all the functionality is implemented) is injected into the methods using Dependency injection.

public function validateProduct(array $data)
{
    $rules = [
        'name' => ['required', 'string', 'min:1', 'max:255'],
        'price' => ['required', 'numeric', 'min:1'],
    ];
    $validator = Validator::make($data, $rules);
    return $validator;
}

public function getProducts()
{
    return ProductResourse::collection(Product::orderBy('id')->get());
}

public function getProduct($request)
{
    $product = Product::where('id', $request->product_id)->first();
    if ($product)
        return new ProductResourse($product);
    return null;
}

public function add($request)
{
    $newProduct = new Product();
    $newProduct->name = $request->name;
    $newProduct->price = $request->price;
    $newProduct->save();
    return $newProduct;
}

public function edit($request)
{
    $newProduct = Product::where('id', $request->product_id)->first();
    $newProduct->name = $request->name;
    $newProduct->price = $request->price;
    $newProduct->save();
    return $newProduct;
}

public function delete($request)
{
    $product = Product::where('id', $request->product_id)->first();
    if ($product) {
        $product->delete();
        return true;
    }
    return false;
}

Thus, a simple application for working with products on Laravel 7 was created and all the requirements in the Pre-employment skills assessment test task for PHP Developer were fulfilled.

You can look at the full code of the project on my Github.

In the future, I plan to sometimes analyze the test tasks that employers give to get the position of PHP Developer. So subscribe to my Twitter not to miss.

Share post
Twitter Facebook