Пример реализации rest api на yii2 + vue.js 2

В данной статье речь пойдет о создании простейшего фронтэнд приложения на vue.js которое взаимодействует с бэкендом на yii2 через api-запросы, используя механизм HTTP Bearer Tokens.

1. Установка и настройка yii2

1.1. Устанавливаем yii2. В моем случае:

composer create-project --prefer-dist yiisoft/yii2-app-basic .

1.2. В конфиге приложения (/config/web.php) у компонента request:

'request' => [
    'parsers' => [
        'application/json' => 'yii\web\JsonParser',
    ]
],

и у компонента urlManager:

'urlManager' => [
    'enablePrettyUrl' => true,
    'enableStrictParsing' => true,
    'showScriptName' => false,
    'rules' => [
 
        '/' => 'site/index',
        'products'  => 'site/index',
        'products/add'  => 'site/index',
        'products/<id:\d+>'  => 'site/index',
        //...
 
	[
            'class' => 'yii\rest\UrlRule',
            'controller' => 'product',
            'prefix' => 'api', //api будет доступен по url, начинающимся с /api/products
        ],
    ],
],

и в /config/db.php вписываем параметры для доступа в бд

1.3. Создаем rest контроллер controllers\ProductController.php, который будет обслуживать все rest запросы:

<?php
 
namespace app\controllers;
 
use yii\rest\ActiveController;
use yii\filters\auth\HttpBearerAuth;
use yii\filters\ContentNegotiator;
use yii\web\Response;
use yii\filters\AccessControl;
 
class ProductController extends ActiveController
{
    public $modelClass = 'app\models\Product';
 
    public function init()
    {
        parent::init();
        // отключаем механизм сессий в api
        \Yii::$app->user->enableSession = false;
    }
 
    public function behaviors()
    {
    $behaviors = parent::behaviors();
 
    $behaviors['authenticator'] = [
            'class' => HttpBearerAuth::className(), //включаем аутентификацию по токену
            'except' => ['options','login'],
	];
 
    return $behaviors;
 
    }
 
 
}

1.4. Создаем ресурсную модель models\Product.php:

<?php
 
namespace app\models;
 
use yii\db\ActiveRecord;
use yii;
 
class Product extends ActiveRecord
{ 
    public static function tableName()
    {
        return '{{products}}';
    }
 
    public function rules()
        {
        return [
            ['title', 'string', 'max'=>255],
            [['price','qt'], 'integer', 'min'=>0],
 
            ['title','filter','filter'=>'strip_tags'],
            ['title','filter','filter'=>'trim'],
            ];
        }
}

1.5. Через командную строку создаем миграцию, для таблицы products

yii migrate/create create_products_table --fields=title:string(255),price:integer,qt:integer

и выполняем ее:

yii migrate

1.6. Для аутентификации на сайте будем использовать данные (а точнее accessToken) из статического свойства $users модели models\User.php:

    private static $users = [
        '100' => [
            'id' => '100',
            'username' => 'admin',
            'password' => 'admin',
            'authKey' => 'test100key',
            'accessToken' => '100-token',
        ],
        //...
    ];

Реализация механизма аутентификации через бд выходит за рамки статьи и не рассматривается.

Бэкенд почти готов, переходим к установке vue.js версии 2

2. Установка и настройка vue.js 2

2.1. В корне проекта создаем файл package.json, за основу можно брать из дистрибутива laravel – package.json плюс добавить vue-router. У меня получился следующий конфиг:

{
    "private": true,
    "scripts": {
        "dev": "npm run development",
        "development": "cross-env NODE_ENV=development node_modules/webpack/bin/webpack.js --progress --hide-modules --config=node_modules/laravel-mix/setup/webpack.config.js",
        "watch": "npm run development -- --watch",
        "watch-poll": "npm run watch -- --watch-poll",
        "hot": "cross-env NODE_ENV=development node_modules/webpack-dev-server/bin/webpack-dev-server.js --inline --hot --config=node_modules/laravel-mix/setup/webpack.config.js",
        "prod": "npm run production",
        "production": "cross-env NODE_ENV=production node_modules/webpack/bin/webpack.js --no-progress --hide-modules --config=node_modules/laravel-mix/setup/webpack.config.js"
    },
    "devDependencies": {
        "axios": "^0.18",
        "bootstrap": "^4.0.0",
        "popper.js": "^1.12",
        "cross-env": "^5.1",
        "jquery": "^3.2",
        "laravel-mix": "^2.0",
        "lodash": "^4.17.4",
        "vue": "^2.5.7",
        "vue-router": "^3.0.1"
    }
}

и устанавливаем необходимые пакеты:

npm install

в результате в корне появится папка node_modules со всеми npm пакетами.

2.2. В корне создаем файл webpack.mix.js, за основу можно брать из дистрибутива laravel – webpack.mix.js. У меня получился следующий конфиг:

let mix = require('laravel-mix');
 
/*
 |--------------------------------------------------------------------------
 | Mix Asset Management
 |--------------------------------------------------------------------------
 |
 | Mix provides a clean, fluent API for defining some Webpack build steps
 | for your Laravel application. By default, we are compiling the Sass
 | file for the application as well as bundling up all the JS files.
 |
 */
 
mix.setPublicPath("./web");
mix.js('vue/app.js', 'web/js');
//mix.sass('resources/assets/sass/app.scss', 'web/css');

2.3. В корне создаем папку vue, в ней будут храниться все создаваемые компоненты.

Внутри создаем app.js:

//require('./bootstrap');
//window.Vue = require('vue');
 
import Vue from 'vue';
import VueRouter from 'vue-router';
 
import App from './components/App.vue';
import ProductsList from './components/ProductsList.vue';
import ProductDetails from './components/ProductDetails.vue';
import ProductAdd from './components/ProductAdd.vue';
 
Vue.component('ProductsList',ProductsList);
Vue.component('ProductDetails',ProductDetails);
Vue.component('ProductAdd',ProductAdd);
 
Vue.use(VueRouter);
 
const routes = [
    {
        path : '/',
        component : ProductsList
    },
    {
        path : '/products/add',
        component : ProductAdd,
    },
    {
        path : '/products/:id',
        component : ProductDetails,
    },
    {
        path : '*',
        component : ProductsList
    }
];
 
const router = new VueRouter ({
    mode: 'history',
    routes
});
 
import axios from 'axios';
axios.defaults.headers.common['Authorization'] = 'Bearer 100-token';
 
new Vue({
    router,
    el: '#app',
    render: h => h(App)
})

Как видно токен доступа берем упомянутый в пункте 1.6.

Далее создаем подпапку components, в ней 4 файла:

– App.vue (рутовый компонент)

<template>
    <div>
        <p>тест yii2 + vue.js</p>
 
        <router-link to="/products/add">Добавить товар</router-link>
 
        <router-view></router-view>
 
        <router-link to="/products">Назад</router-link>
    </div>
</template>
 
<script>
 
export default {
 
  data () {
    return {
    }  
  },
 
  mounted: function () {
    //console.log('App component mounted');
  }
 
}
</script>
 
<style>
 
</style>

– ProductAdd.vue (форма создания продукта и отправка данных на сервер)

<template>
    <div class="product">
        <h1>Добавление товара</h1>
        <form>
            <div class="form-group">
                <label for="title">Название</label>
                <input type="text" class="form-control" id="title" placeholder="Введите название товара" v-model="product.title" />
            </div>
            <div class="form-group">
                <label for="price">Цена</label>
                <input type="number" class="form-control" id="price" placeholder="Введите цену за единицу товара" v-model="product.price" />
            </div>
            <div class="form-group">
                <label for="qt">Количество</label>
                <input type="number" class="form-control" id="qt" placeholder="Введите количество товара" v-model="product.qt" />
            </div>
            <div class="form-group">
                <button type="submit" class="btn btn-primary" @click.prevent="addProduct()">Создать</button>
            </div>
        </form>
    </div>
</template>
 
<script>
import axios from 'axios';
 
export default {
    data() {
        return {
            product: {
                title: 'test product',
                price: 123,
                qt: 6,
            }
        }
    },
    methods: {
        addProduct() {
            axios({
                    method: 'post',
                    url: '/api/products',
                    responseType: 'json',
                    data: this.product,
                    headers: {
                        'Content-type': 'application/json; charset=UTF-8',
                        }
                    }
                )
              .then(response => (
                    //this.product = response.data
                    this.$router.push('/')
                    )
                )
              .catch(error => console.log(error));
 
 
        }
    },   
 
 
    }    
</script>
 
<style scoped>
    .product {
        border: 1px solid grey;
        border-radius: 10px;
        margin: 10px 0;
        padding: 10px;  
    }
</style>

– ProductDetails.vue (просмотр одиночного продукта, получение информации о товаре через api)

<template>
    <div class="product">
        <h1>Просмотр товара с ID={{product.id}}</h1>
        <form>
        <div>
            <label for="title">Title</label>
            <span>{{product.title}}</span>
        </div>
        <div>
            <label for="price">Price</label>
            <span>${{product.price}}</span>
        </div>
        <div>
            <label for="qt">Quantity</label>
            <span>{{product.qt}}</span>
        </div>
        </form>    
    </div>
</template>
 
<script>
import axios from 'axios';
 
export default {
    data() {
        return {
            product: {}
        }
    },
    created() {
        this.loadProductDetails(this.$route.params.id);
    },
 
    methods: {
        loadProductDetails(id) {
            axios
              .get("/api/products/"+id)
              .then(response => (this.product = response.data))
              .catch(error => console.log(error));                
        }
    },
 
    watch: {
        '$route.params.id' (id) {
            this.loadProductDetails(id);
        }
    }
 
    }    
</script>
 
<style scoped>
    .product {
        border: 1px solid grey;
        border-radius: 10px;
        margin: 10px 0;
        padding: 10px;  
    }
</style>

– ProductsList.vue (просмотр списка продуктов, получение информации о товарах через api)

<template>
    <div class="products">
        <h1>Список товаров</h1>
	<ul>
            <li v-for="product in products" :key="product.id">                
                <a href="#" class="" @click="viewProductDetails(product.id)">{{product.title}}</a>
            </li>
	</ul>
    </div>
</template>
<script>    
import axios from 'axios';
 
export default {
    data() {
      return {
          products: [],
      }  
    },    
    created() {        
        this.loadProductsList();
    },
    methods: {
        loadProductsList() {
            axios
              .get("/api/products")
              .then(response => (this.products = response.data))
              .catch(error => console.log(error));            
        },
        viewProductDetails(id) {           
            this.$router.push('products/'+id);
        }
    }
}
</script>
 
<style scoped>
    .products {
        border: 1px solid grey;
        border-radius: 10px;
        margin: 10px;
        padding: 10px;  
    }
</style>

2.4.

Пробуем собрать:

npm run dev

Если нет ошибок, то в директории /web/app.js создастся файл, который необходимо подключить в нужном представлении. Например в /view/site/index.php:

<div id="app"></div>
<?php $this->registerJsFile('/js/app.js'); ?>

Проверяем в браузере что получилось по адресу http://ваш_домен/products. Заполняем бд, добавляя товары. Проверяем уходят ли ajax запросы на сервер.

Поддерживаемые api-запросы:

  • GET /api/products: список всех товаров;
  • POST /api/products: создание нового товара;
  • GET /api/products/123: подробная информация о товаре 123;

Ссылка на репозиторий

Комментарии:

  1. Александр

    Спасибо большое за статью.

  2. Slava

    Спасибо.
    Только добавьте папку vendor на Github)