Автореферат

Наименование Ресурсно-компонентное программирование

Автор А.В.Михалькевич

Специальность

Ресурсно-компонентное программирование - это особый архитекутрный шаблон проектирования, используемый при разработке web-приложений с чётким разделением приложения на бэкенд и фронтенд. Ресурсы - это ответы бэкенда в формате JSON. Фронтенд, в свою очередь, взаимодействует с бэкендом только через компоненты. Отсюда и название - ресурсно-компонентное программирование.

В публикации рассматриваются особенности разработки и использования ресурсов и ресурсных классов на примере бэкенд фрэймворка Laravel.

,

Анотация

Разработка фронтенд с помощью Vue.js и бэкенда с помощю Laravel. Закрытие и открытые маршруты. Взаимодействие фроентенд и бэкенда.

Anotation in English

Development of frontend using vue.js and backend with help Laravel. Protected and open routes. Interaction of frientend and backend.

Ключевые слова laravel, vue, routes, mvc, mvc, hmvc, oop, ООП

Количество символов 102477

Содержание

Введение

В последнее время крупные web-приложения всё чаще разрабатываются с разделением на бэкенд и фронтенд. При такой разработке на бэкенд возлагаются задачи создания и обслуживания ресурсов. А на фронтенд возлагаются задачи разработки компонентов и умение взаимодействовать с ресурсами бэкенда. Отсюда - ресурсно-компонентное программирование.

1 Обоснования ресурсно-компонентного программирования

Если в объектно-ориентированном программировании главным структурным элементом является объект, то в компонетно-ресурсном - компонент и ресуср. Компоненты формируются на стороне фронтенд, ресурсы - на строне бэкенд.

Например, такие фрэймворки как Laravel позволят разрабатывать целостное web-приложение (без разделения на бэкенд и фронтенд части), тогда используется архитектурный шаблон проектирвания HMVC (Hierarchical model–view–controller).

Но для разработки бэкендов такой шаблон проектирования не подходит, т.к. у бэкендов отсутствует V (view) из HMVC. Вместо шаблона бэкенд отдаёт ответ ввиде ресуров (Resource) с ответами в формате JSON. Laravel позволяет реализовывать такие ответы либо с помощью специальных методов (response(), json(), toArray() и др), либо с помощью специальных ресурсных классов.

Схематично и упрощённо такой подход в проектировании web-приложений можно представить следующим образом:

Так в ресурсно-компонентном программировании разрабатываются сразу два приложения: фронтенд и бэкенд. Фронтенд отправляет запрос пользователя в свой маршрутизатор, который в свою очередь перенаправляет запрос в маршрутизатор Laravel, который в зависимости от запроса вызывает тот или иной контроллер. В контроллере реализуется вся бэкенд логика, в том числе и обращение к модели (или к другим вспомагательным классам). Получив и преобразовав данные, контроллер их перенаправляет в ресурс, который возвращается в фронтенд.

Как в промышленности, так и в программировании, ресурсы - это то, что мы используем для производства. Модели можно представить ввиде полезных ископаемых. Тогда задача контроллера - это преобразование полезных ископаемых из недр моделей (данных) в ресурсы, которые могут быть использованы клиентом.

Со стороны фронтенда всё выглядит просто - запрос на бэкенд возвращает овет в формате JSON, который необходимо преобразовать в HTML. Используется ли на бэкенде сложная логика получения данных, или идёт обращение к JSON-файлу, фронтенду должно быть всё-равно, лишь бы быстро получить ответ с нужными данными.

В современных фронтенд-фрэймворках такие запросы на бэкенд и обработку ответов удобно реализовывать в компонентах. Тогда, компонент является инициатором запроса на бэкенд и получает ответы бэкенда.

 

1 .1 Жизненный цикл запроса

В ресурсно-компонентном программировании жизненный цикл запроса проходит через две основные стадии: frontend и backend. Компоненты фронтенда получают ответы бэкенда через ресурсы.

Схематично жизненный цикл запроса можно представить следующим образом:

1 .2 Ресурсно-компонентное программирование в командной разработке

Ресурсно-компонентное программирование больше подходит для командной разработки средних и крупных проектов. Команда должна состоять из бэкендера, фронтендера, дизайнера и тестировщика. 4 человека + тимлид это уже команда. Если в команде больше людей, то несколько человек могут заниматься бэкендом, несколько фронтендом и т.д.

1 .3 Ресурсы

Ресурсы - это преобразованные данные предназначенные для клиента.

Ресурсы формируются на сторне бэкенда, и должны формироваться в рамках оговорённых стандартов. Общепринятым форматом ресурсов является формат Json.

1 .4 Компоненты

На низком уровне, компоненты представляют собой изолированные части (блоки) пользовательского интерфейса (UI). В отдельные компоненты выделяются:

- большие обособленные структуры html, css и javascript (примеры: модальное окно, галерея);

- часто повторяемые структуры html, используемые с разными данными (примеры: статья, товар).

- элементы форм и схожих структур.

Компонент может взаимодействовать с другими компонентами.

Возьмем, к примеру, html-элемент. Мы можем использовать этот элемент с любыми технологиями в браузере, и мы можем передавать такие свойства, как width и height и слушайте такие события, как onclick.

На высоком уровне, мы можем сказать, что компоненты — это набор API-интерфейсов веб-платформы. (интерфейсы прикладного программирования), которые позволяют нам создавать теги HTML, со встроенными стилями и JavaScript логикой, которые будут работать в современных веб-браузерах и может использоваться фрэймворками JavaScript (React, Angular, Vue.js и т. д.).

Ожидается, что компоненты будут использоваться в веб-браузерах в течение длительного времени и предлагают множество преимущества, в том числе следующие:

• Веб-компоненты можно использовать повторно и работать между платформами.

• Веб-компоненты могут работать во всех основных веб-браузерах.

• Веб-компоненты просты в обслуживании и готовы к будущем, если они основаны на спецификациях веб-платформы.

Каждый компонент, не зависимо от того, к какому типу он относится обладают одним интерфейсом общения со всей системой. Однако, по способу подключения, можно выделить следующие типы компонентов:

- базовый (App.vue). Это главный компонент приложения, содержащий базовый шаблон или структуру приложения.

- маршрутные (это компоненты, вызываемые маршрутизатором, папка routes)

- вспомогательные компоненты.

2 Основы разработки фронтенда

Современные фронтенд-фрэймворки позволяют разделить приложения на компоненты.  Так, проектирование программных приложений идёт путем построения независимых компонентов. Каждый компонент имеет интерфейс общения с остальной частью системы. Такую компонентную разработку нам предлагают фрэймворки Angular, React и Vue, Nuxt.

2 .1 Node и менеджеры зависимостей

Сперва необходимо установить node.js

Информация по установке - https://nodejs.org/en/download

В Ubuntu node.js можно установить с помощью репозитория apt:

Убедтиться в том, что node установлен, можно с помощью этой команды

node -v

Так же давайте проверим менеджер пакетов для node - npm

npm -v

Npm устанавливается следующим образом:

sudo apt install npm

Npx - еще один, альтернативный менеджер пакетов для Node. Проверяем версию также:

npx -v

2 .2 Создание и запуск фронтенда

Создать фронтенд можно множеством способов, нужно лишь выбрать фрэймворк или шаблон проектирования. Большинство из способов предполагает первоначальную установку фрэймворка. Фрэймворк предоставляет структуру проекта, под которую разработчику нужно подстроиться, изучить фрэймворк и ту структуру приложения.

Начнём разработку с файла package.json.

Создаем папку для будущего проекта, в ней файл package.json со следующим содержимым:

{
"name": "nuxt-app",
 "scripts": {
  "dev": "nuxt"
 }
}

Список всех исполняемых скриптов и зависимостей приложения хранятся в этом файле.

Для установки всех зависимостей воспользуемся командой

npm i

Теперь, чтобы запустить проект, необходимо запустить следующую команду:

npx naxi dev

Теперь главная страница сайта доступна по порту 3000

http://localhost:3000

2 .3 Структура фронтенда

Основные папки фронтенда: components, assets, static. Создать их можно одной командой:

mkdir components assets static

Также в корне проекта необходимо создать файл nuxt.config.js:

touch nuxt.config.js

Рассмотрим начальную структуру проекта. Подробнее о каждой папке.

2 .3 .1 pages

Дирректория pages содержит маршрутные компоненты. Каждому файлу с расширением .vue этой папки Nuxt формирует соответствующий маршрут.

Маршруту главной страницы соответствует компонент index.vue. Все маршрутные компоненты создаём в этой папке. Например: about.vue, contacts.vue ...

Ссылку на такой маршрут лучше формировать с помощью специального в тэга NuxtLink.

2 .3 .2 components

Дирректория components - папка для хранения маршрутных компонентов Vue.js.

Здесь мы можем создавать свои компоненты и импортировать файлы  .vue. 

2 .3 .3 assets

Данный каталог содержит нескомпилированные активы, такие как файлы Stylus или Sass, изображения или шрифты. В template ссылка на изображение может выглядеть так:

src="~/assets/your_image.png"

Подробнее - по ссылке Документация Nuxt

2 .3 .4 plugins

Автоматическое подключение плагинов из этой папки в проект. Сперва необходимо создать плагин в этой папке. Затем подключить его в файле nuxt.config.js.

Для реализации запросов на бэкенд, можно воспользоваться Axios.

Установка модуля:

npm install @nuxtjs/axios

После чего, создайте файл axios.js со следующим содержимым:

 import axios from 'axios';
//import {store} from  '@/store/index';
export default defineNuxtPlugin(nuxtApp => {
    //console.log(store.state.token);
    axios.defaults.baseURL = 'http://localhost:8000/api/';
    axios.defaults.headers["content-type"] = "application/json";
    axios.defaults.headers.common.authorization = `Bearer `;
    axios.defaults.headers.common['Access-Control-Allow-Origin'] = '*';
});


Подключение плагинов в файле nuxt.config.js

export default {
    css: ['~/assets/css/main.css'],
    modules: ['@nuxtjs/axios'],

    plugins: ['~/plugins/axios.js']
}

2 .4 Запросы на бэкенд и авторизация

Для запросов на бэкенд используем axios. Авторизация через заголовок Bearer + токен.

2 .4 .1 Axios

При обработке данных, получаемых по ссылке есть несколько особенностей: необходимо выполнить запрос на бэкенд, а для выполнения запроса лучше использовать асинхронную функцию. Для выполнения запросов есть инструмент axios.

npm install axios

https://github.com/axios/axios - По этой ссылке официальная документация

Функции, в которых будем делать запросы на бэкенд лучше делать асинхронными, и можем помещать их в специальное свойство экспорта methods

import axios from "axios";
export default {
  name: 'User',
  data() {
    return {
      users: [],
    };
  },
  methods: {
    async load() {
      const { data } = await axios.get(`https://randomuser.me/api/?results=6`);
      this.users = data.results;
    },
  },
  beforeMount() {
    this.load();
  },
}

После того, как переменная users будет сгенерирована, в тэге templates с ней можно работать как с обычными массивами (если возвращается массив, с использованием атрибута v-for) или выводить значения через объект.

Можем усовершенствовать функцию load(), добавив входящий параметр. Так же обратите внимание, что мы можем не использовать beforeMount, а вызывать её непосредственно, в методе data():

import axios from "axios";
export default {
  name: 'User',
  data() {
    this.load(6);
    return {
      users: null,
    };
  },
  methods: {
    async load(number) {
      const data = await axios.get(`https://randomuser.me/api/?results=${number}`);
      this.users = data.data.results;
    },
  },
}

2 .4 .2 Глобальные настройки axios

Глобальные настройки для axios можем хранить в корневом файле main.js

import axios from 'axios';
...
axios.defaults.baseURL = 'http://localhost:8000/api/';
axios.defaults.headers["content-type"] = "application/json";
axios.defaults.headers.common.authorization = `Bearer ${localStorage.getItem('token')}`;
axios.defaults.headers.common['Access-Control-Allow-Origin'] = '*';

 

2 .5 Composition API

C 2021 года синтаксис script setup стал рекомендуемым способом создания нового проекта на Vue.

2 .5 .1 Основы Composition API

Новый Composition API создает много удобных способов переиспользования кода в компонентах. Вспомним, что во Vue 2 логика разделялась по опциям: data, methods, props, created, и так далее:

export default {
  name: "FormSelect",
  data(){
    return {SelectedValue:''}
  },
  props: {
    options: {
      type: Array,
      default: () => []
    }
  },
  methods: {
    changeOptions(){
      this.$emit('update:modelValue', this.SelectedValue)
    }
  }
}

С Composition API мы не ограничены этой структурой и не обязаны разделять код по опциям. Пример компоноента AuthLogin:

import {ref} from "vue";
import axios from "axios";

export default {
  name: "AuthLogin",
  setup(){ 
    const email = ref('');
    const password = ref(''); 
    const submit = async () => {
      const user_data = {
        'email' : email.value,
        'password' : password.value,
      };
      const response = await axios.post('login', user_data);
      await localStorage.setItem('token', response.data.token);
      //console.log(response.data.user.name, response.data.token);
      document.location.href = '/home';
    }
    return { 
      email,
      password, 
      submit
    }
  }
}

2 .5 .2 Компонент видео-плеера с помощью синтаксиса script setup

Vue 3.2 ввел новый синтаксис <script setup>

Разработаем компонент видео плеера используя такой синтаксис.

Содержимое тега template:

Стили, в тэге style:

#player {
  width: 740px;
  margin: 20px auto;
  padding: 10px 5px 5px 5px;
  background: black;
  border: solid 1px rgba(134, 201, 148, 0.37);
  border-radius: 3px;
}
#play, #mute {
  padding: 2px 10px;
  width: 65px;
  border:solid 1px grey;
  font-weight:bold;
  border-radius: 3px;
}
#buttons {
  float:left;
  height: 20px;
}
#bar {
  float:left;
  width: 400px;
  height: 20px;
  padding: 1px;
  margin: 2px 5px;
  border:solid 1px darkgreen;
  background: white;
}
#progress {
  width: 0;
  height: 18px;
  background: rebeccapurple;
}
.clear {
  clear: both;
}

А вот и сам скрипт, который должен находится в тэге <script setup>:

import {ref, onMounted} from "vue";
    const media = ref();
    const maxim = ref(400);
    const play = ref();
    const progress = ref();
    const bar = ref();
    const loop = ref();
    const volume = ref();
    const mute = ref();
    onMounted(async ()=>{
      media.value = document.getElementById('media');
      progress.value = document.getElementById('progress');
      mute.value = document.getElementById('mute');
      play.value = document.getElementById('play');
      bar.value = document.getElementById('bar');
    });
    const push = () => {
      if(!media.value.paused && !media.value.ended){
        media.value.pause();
        play.value.value = 'Play';
        clearInterval(loop);
      }else{
        media.value.play();
        play.value.value = 'Pause';
        loop.value = setInterval(status, 1000);
      }
    };
    const status = () =>{
      if(!media.value.ended) {
        var size = parseInt(media.value.currentTime * maxim.value / media.value.duration);
        progress.value.style.width = size + 'px';
      }else{
        progress.value.style.width = 0;
        clearInterval(this.loop);
        play.value.value = 'Play'
      }
    };
    const sound = () => {
      if(mute.value.value == 'Mute'){
        media.value.muted = true;
        mute.value.value = 'Sound';
      }else{
        media.value.muted = false;
        mute.value.value = 'Mute';
      }
    };
    const level = () => {
      media.value.volume = volume.value.value;
    }

В результате получим компонент с таким плэеером:

2 .6 Основы компонентов

Компоненты это переиспользуемые экземпляры Vue. Компоненты принимают такие опции, как как data, computed, watch, methods, хуки жизненного цикла и их можно переиспользовать множество раз. Например, у вас могут быть компоненты для заголовка, боковой панели, контента, каждый из которых может содержать другие компоненты для ссылок, постов блога...

2 .6 .1 Особенности компонентов Vue

Vue позволяет нам создавать интерфейсные веб-приложения с компонентами. С ними мы можем разделить наше приложение на маленькие, повторно используемые части, которые составляются вместе, чтобы сделать большое приложение. Этот композиция выполняется путем вложения. Чтобы разные части приложения составлялись вместе, мы может передавать данные между ними. Компоненты можно брать из библиотек, а также можем создавать сами.

Компоненты - это файлы с расширением vue, которые обычно хранятся в папке /src/components. Компоненты могут принимать значения из родительских компонетов и могут быть использованы для передачи данных в дочерние компоненты.

После установки router-view появляется такой тип компонента, как - маршрутный. Маршрутный компонент - это компонент ленивой подгрузки в тэг router-view. Вспомогательный компонент - все остальные компоненты шаблона.

Компонент состоит из:

- тэга template, внутри которого содержится html-код и vue-тэги других компонентов.

- скрипты, содержащиеся в тэге script

- стили в тэге style

Первоначальной задачей после установки фронтенда, необходимио разбитие шаблона на компоненты. Но, сперва визуально разделим страницу шаблона на две части - меняющуюся часть и неизменную. Часто, к неизменной относится меню, шапка и подвал сайта. Так же, часто элементы меню лучше переносить в отдельный вспомогательный компонент Menu.vue

2 .6 .2 Вывод переменных

В компонент переменные могут поступать разными способами. Но, не зависимо от способа поступления переменной и её типа, для вывода их значений в тэге template можно воспользоваться диррективой {{}}.

Вывод переменной строки или числа:

{{user_name}}

Вывод переменной объекта

{{user.name}}

Вывод массива


 

Hello, {{ user.name }}, from {{ user.location.city }}!


Обратите внимание, что если нужно добавить значение переменной в атрибут к какому-нибудь тэгу, для этого можно воспользоваться диррективой :.

 :key="user.id"

2 .6 .3 Импорт компонентов

Импортировать компонент можно из любого другого компонента. Например, импорт вспомогательных компонентов в базовом компоненте App.vue:

import MenuTop from "@/components/templates/MenuTop";
import BootstrapFooter from "@/components/templates/BootstrapFooter";
export default {
  components:{
    MenuTop,
    BootstrapFooter
  }
}

Глобальная регистрация компонентов в файле main.js

import components from '@/components/ui';
const app = createApp(App);
components.forEach(component => {
    app.component(component.name, component);
})
app.use(router).mount('#app');

2 .6 .4 Два способа создания переменных

Переменные для компонентов можем создавать в тэге script,

Для создания переменных мы можем использовать метод data:

export default {
  data() {
    return {
      name: "test",
    }
  }
}

Тогда в template мы можем вывести значение:

Hello, {{ name }}!

Vue3

Во Vue3 для создания переменных можно воспользоваться модулем ref. Пример:

import {ref, onMounted} from "vue";
import axios from 'axios';

export default {
  name: 'About',
  setup() {
    const users = ref([]);
    onMounted(async () => {
      const response = await axios.get('https://randomuser.me/api/?results=6');
      users.value = response.data.results;
      console.log(users.value);
    });
    return {
      users
    }
  }
}

Обратите внимание на то, что после объявления переменной users, мы можем к ней обращаться с помощью users.value, например для того чтобы определить или переопределить её значение.

Такой способ создания и работы с переменными (с помощью модуля ref и в диррективе setup используется в Composition API)

2 .6 .5 Разработка библиотеки компонентов

Из часто используемых элементов лучше сделать библиотеку. Поэтому рассмотрим, как разработать свою библиотеку компонентов, куда будут входить элементы форм и диалоговое окно.

2 .6 .5 .1 Структура библиотеки компонентов

Библиотечные компоненты будем хранить в папке ui, дирректории components

2 .6 .5 .2 Компонент диалогового окна

Сперва в файле MyDialog заполним template

Диалоговое окно состоит из двух элементов: элемента с классом dialog и элемент с классом dialog_content. dialog - это одновременно и фон для диалогового окна, и элемент содержащий контент диалогового окна. Отображение или скрытие этого элемента зависит от переменной show. А по клику на фон, будет вызываться функция hideDialog, которая закроет окно. Событие клик не должно распространяться на контентную часть диалогового окна, поэтому в элементе dialog_content добавим @click.stop.

Добавим скрипт и стили диалогового окна.

Script

export default {
  name: "MyDialog",
  props: {
    show: {
      type: Boolean,
      default: false
    }
  },
  methods:{
    hideDialog(){
      this.$emit('update:show', false);
    }
  }
}

Style

.dialog {
  top: 0;
  left: 0;
  right: 0;
  background: rgba(0, 0, 0, 0.5);
  position: fixed;
  display: flex;
  height: 100%;
}
.dialog_content {
  margin: auto;
  background: white;
  border-radius: 12px;
  min-height: 50px;
  min-width: 500px;
  padding: 20px;
}

2 .6 .5 .3 Компонент элемента формы input

Сперва заполним template

Script:

export default {
  name: "FormInput",
  props:['modelValue', 'typeInput'],
  methods:{
     updateInput(event){
       this.$emit('update:modelValue', event.target.value)
     }
  }
}

2 .6 .5 .4 Компонент элемента формы select

Template:

Script

export default {
  name: "FormSelect",
  data(){
    return {
      SelectedValue:''
    }
  },
  props: {
    options: {
      type: Array,
      default: () => []
    }
  },
  methods: {
    changeOptions() {
      this.$emit('update:modelValue', this.SelectedValue);
    }
  }
}

2 .6 .5 .5 Компонент элемента формы button

Template

Script

export default {
  name: "FormButton",
  props:['nameValue'],
}

2 .6 .5 .6 Глобальное подключение компонентов

Библиотечные компоненты мы можем сделать глобальными. Тогда при использовании их не нужно будет импортировать. Для этого в папке ui создадим файл index.js со следующим содержимым:

import MyDialog from "@/components/ui/elements/MyDialog";
import FormInput from "@/components/ui/forms/FormInput";
import FormSelect from "@/components/ui/forms/FormSelect";
import FormButton from "@/components/ui/forms/FormButton";

export default [
    MyDialog,
    FormInput,
    FormSelect,
    FormButton
];

Т.е. в этом файле подключим все библиотечные компоненты.

Далее немного перепишем файл корневой файл main.js

...
import components from '@/components/ui';
...
const app = createApp(App);
components.forEach(component => {
    app.component(component.name, component);
})
app.use(router).mount('#app');

В этом файле мы подключим index.js из нашей библиотеки. Потом создадим объект app - объект приложения. И с помощью цикла forEach пройдёмся по всем библиотечным компонентам, добавляя их к приложению.

2 .6 .5 .7 Использование библиотечных компонентов

Теперь в любом компоненте приложения мы можем использовать библиотечные компоненты без импортирования. Например так:

Script в Composition API.

import axios from "axios";
import {onMounted, ref, watch} from "vue";

export default {
  name: 'ProductsView',
  setup() {
    const catalogs = ref([]);
    const DialogVisible = ref(false);
    const SelectedOption = ref('');
    const ProductName = ref('');
    const ProductDescription = ref('');
    const DialogShow = () => {
      DialogVisible.value = true;
    };
    const SendData = () => {
      console.log(ProductName, ProductDescription, SelectedOption);
    };
    onMounted(async () => {
      const response = await axios.get('catalogs');
      catalogs.value = response.data;
      console.log(catalogs.value);
    });
    watch(SelectedOption, (newValue) => {
      console.log(newValue);
    });
    return {
      catalogs,
      DialogVisible,
      SelectedOption,
      ProductName,
      ProductDescription,
      DialogShow,
      SendData
    }
  },
}

3 Основы разработки бэкенда на Laravel

Для разработки бэкенд API воспользуемся фрэймворком Laravel. Но т.к., за отображение элементов страниц сайта отвечает фронтенд, то основной задачей Laravel будет формирование ресурсов, к которым будет обращаться фронт за данными.

3 .1 Требования к бэкенду

Со стороны фронтенда, на бэкенд наккладываются следующе требования

1. на каждый запрос фронтенда (request) бэкенд должен отдать ответ (respnse) в формате Json! никаких перенаправлений (redirect) и шаблонов (blade)

2. никаких лишних данных: например, если фронтенд запрашивает email и name пользователя, то в идеале, в ответе должны быть только эти данные.

3. скорость: чем быстрее тем лучше.

4. формат данных: используем разные типы запросов для разных действий. Get- вывод данных, Post - добавление данных, Put -обновление данных и Delete - удаление данных.

5. тестирование: рабочие маршруты на бэкенд не только должны быть протестированы, но и добавлены в Postmen

 

3 .2 Начало работы с Laravel

Официальный сайт laravel – http://laravel.com

Перед установкой убедитесь в том, что у вас уже установлены:

- PHP актуальной версии (в некоторых операционных системах может понадобиться ручная установка следующих модулей: Mcrypt PHP Extension OpenSSL PHP Extension MbstringPHPExtension;

- база данных (MySQL);

- менеджер зависимостей Composer.

 

3 .2 .1 Установка Laravel и создание проекта

Также перед установкой необходимо убедиться в наличии менеджера зависимостей composer и обновить его с помощью следующей команды:

composer self-update

Установка Laravel

Сперва переходим в корневую папку сервера. Для Windows OpenServer - эта папка domains:

cd domains

Для Linux - это папка www.

Для Mac - папка htdocs:

После чего запускаем команду установки:

composer create-project laravel/laravel

Таким образом в текущей папке появится новая папка laravel с файлами проекта. В дальнейшем её можно переименовать. Однако, если есть необходимость сразу установить фрэймворк в папку с оределенным именем, можно воспользоваться другой командой:

Composer create-project laravel/laravel your-project-name

3 .2 .2 Запуск проекта

Если вы используете сервер Apache, то в корне проекта создайте файл без имени с расширением .htaccess и со следующим содержимым:

    RewriteEngine On
    RewriteRule ^(.*)$ public/$1 [L]

Если вы используете сервер Nginx, воспользуйтесь следующим конфигурационным файлом:

server {
        listen 80 default_server;
        listen [::]:80 default_server;
 
         # Log files for Debugging
         access_log /var/log/nginx/laravel-access.log;
         error_log /var/log/nginx/laravel-error.log;
 
         # Webroot Directory for Laravel project
         root /var/www/laravel/public;
         index index.php index.html index.htm;
 
         # Your Domain Name
         server_name localhost;
 
         location / {
                 try_files $uri $uri/ /index.php?$query_string;
         }
 
         # PHP-FPM Configuration Nginx
         location ~ \.php$ {
                 try_files $uri =404;
                 fastcgi_split_path_info ^(.+\.php)(/.+)$;
                 fastcgi_pass unix:/run/php/php7.3-fpm.sock;
                 fastcgi_index index.php;
  		         fastcgi_read_timeout 1500000;
                 fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
                 include fastcgi_params;
         }
 }

Таким образом мы перевели все запросы клиента в папку public и закрыли доступ ко всем остальным папкам проекта.

Однако, Laravel содержит свой встренный сервер, и мы можем запустить проект без Apache, Nginx или других сторонних серверов.

php artisan serve

3 .2 .3 Консоль Artisan

Artisan - это интерфейс командной строки (CLI) входящий в состав Laravel. Он предоставляет ряд команд, которые будут полезны при разработке вашего приложения. Чтобы посмотреть список всех доступных Artisan-команд, вы можете воспользоваться командой list:

php artsian list
Каждая команда так же содержит "подсказку", которая отображает и описывает все возможные аргументы и опции доступные для команды. Чтобы увидеть подсказку, просто напишите перед названием команды слово help:
php artisan help migrate

3 .3 База данных

С базой данных Laravel взаимодействует с помощью  разных движков: query builder или Eloquent ORM. На данный момент Laravel поддерживает четыре системы баз данных: MySQL, PostgreSQL, SQLite и SQL Server

3 .3 .1 Подключение к базе данных

Настройки подключения к базе данных находятся в файле .env

DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=6603
DB_DATABASE=laravel
DB_USERNAME=root
DB_PASSWORD=helloworld

3 .3 .2 Конструктор запросов

Построитель запросов (Query builder) Laravel предлагает интерфейс для создания и выполнения запросов к базе данных. Его можно использовать для выполнения большинства операций с базой данных в вашем приложении и он отлично работает со всеми поддерживаемыми Laravel системами баз данных.

Вы можете использовать метод table фасада DB, чтобы начать запрос. Метод table возвращает текущий экземпляр построителя запросов для данной таблицы, позволяя вам связать больше ограничений к запросу

$cat = DB::table(‘categories’);

Рассмотрим реализацию CRUD с помощью построителя запросов

SELECT

Для создания SELECT-запросов, сперва необходимо создать объект классаDB и статического метода table(), входящим параметром в который необходимо передать имя таблицы.

Для получения результата можно воспользоваться следующими методами:

  • get() его мы только что использовали, он возвращает массив объектов — результирующих строк с полями;
  • first() этот метод вернёт один объект-результат, который подошёл под критерии запроса;
  • find($id)  метод находит запись по её ID; это краткая форма для записи where('id', '=', $id); возвращает один объект-результат;
  • only($fieldname)  возвращает значение одного поля, подходящего под запрос;
  • get(array(…))мы можем передать методу get() массив полей, чтобы получить только их.

Получить все поля таблицы можно, используя метод get().

$cat = DB::table(‘categories’)->get();

Уточнение запроса осуществляется с помощью метода where()

$cat = DB::table(‘categories’)->where(‘showhide’, ‘=’, ‘show’)->get();

Конструктор запросов может быть использован для выборки данных из нескольких таблиц через JOIN.

DB::table('users')
            ->join('contacts', 'users.id', '=', 'contacts.user_id')
            ->join('orders', 'users.id', '=', 'orders.user_id')
            ->select('users.id', 'contacts.phone', 'orders.price');

Если необходимо сделать сложную фильтацию, то в значение методов where()или orWhere() можно добавлять функуию с дополнительным query-запросом:

 DB::table('users')
            ->where('name', '=', 'Джон')
            ->orWhere(function ($query) {
                $query->where('votes', '>', 100)
                      ->where('title', '<>', 'Админ');
})->get();

INSERT

DB::table('users')->insert(
    array('email' => 'john@example.com', 'votes' => 0)
);

Если необходимо вставить данные и при этом тут же получить id, можно воспользоваться методом insertGetId.

$id = DB::table('users')->insertGetId(
    array('email' => 'john@example.com', 'votes' => 0)
);

Laravel также поддерживает множественную вставку данных:

DB::table('users')->insert(array(
    array('email' => 'taylor@example.com', 'votes' => 0),
    array('email' => 'dayle@example.com', 'votes' => 0),
));

UPDATE

Для обновления данных можно воспользоваться методом update().

DB::table('users')->where('id', 1)
                  ->update(array('votes' => 1));

Однако, обновление можно осуществить и с помощью метода save()

$obj = DB::table('users')->where('id', 1);
$obj->votes = 1;
$obj->save();

DELETE

 DB::table('users')->where('votes', '<', 100)->delete();

 DB::update('update users set votes = 100 where name = ?', array('John'));

Выполнение запросов другого типа

 DB::statement('drop table users');

Транзакции

Для выполнения запросов внутри одной транзакции, можно воспользоваться методом transaction.

DB::transaction(function(){
    DB::table('users')->update(array('votes' => 1));
    DB::table('posts')->delete();
});

Доступ к соединениям

При использовании нескольких подключений к БД, вы можете получить к ним доступ через метод  DB::connection:

$users = DB::connection('foo')->select(...);
$pdo = DB::connection()->getPdo();
// получение объектаPDO указанног соединения.
DB::reconnect('foo');
// переподключение к базе данных

Если вам нужно отключиться от БД - например, чтобы не превысить лимит max_connections в БД, вы можете воспользоваться методом disconnect

DB::disconnect('foo');

По умолчанию, Laravel записывает все SQL-запросы в памяти, выполненные в рамках текущего HTTP-запроса.

Однако, в некоторых случаях, как, например, при вставке большого количества записей, это может быть слишком ресурсозатратно. Для отключения журнала вы можете использовать метод disableQueryLog:

DB::connection()->disableQueryLog();

Для получения массива выполненных запросов используйте метод getQueryLog:

$queries = DB::getQueryLog();

3 .3 .3 Миграции

Миграции базы данных являются весьма полезны для любого проекта, особенно для проектов с несколькими разработчиками, позволяя иметь последнюю версию базы данных у всех разработчиков. В Laravel для этого достатчно выполнить одну команду в командной строке.

Для создания новой миграции нам понадобится интерфейс командной строки Laravel — «Artisan».

Итак, откроем консоль командной строки из папки, где расположен файл artisan. В консоли введем следующую команду:

php artisan make:migration create_categories

Консоль должна ответить нам следующей фразой:

“Great! New migration created!”

Если получили такой ответ, то перейдем в папку application/migrations. Там должен находится файл 2014_04_20_210359_create_users.php (к имени миграции artisan добавляет текущую дату). Откроем данный файл. Увидим следующее:

Class Create_Categories {

  /**

  * Внести изменения в базу данных.

  * @return void

  */

  public function up()

  {
    //
  }
 
  public function down()

  {
    //
  }

}

Класс миграции содержит два метода up() для внесения изменений в таблицу базы данных и down() для отмены действий метода up(). Например, если мы создаем таблицу в up(), то в down() ее нужно удалить.

Допишем действия up() и down()

public function up(){

Schema::create('categories', function ($table) {

    // auto incremental id (PK)
    $table->increments('id');

    // varchar 32
    $table->string('name', 32);

    // enum
    $table->enum('showhide', array('show','hide'));

    // created_at | updated_at DATETIME
    $table->timestamps();

 });                   

}

public function down(){

 Schema::drop('categories');

}

Внутри функции мы можем использовать следующие красивые методы для определения структуры таблицы:

  • increments() — добавить автоинкрементируемое поле — его будет иметь бо?льшая часть ваших таблиц
  • string() — создать поле VARCHAR — правда, «строка» куда более описательное имя, чем в стандарте SQL?
  • integer() — добавить целочисленное поле
  • float() — поле с дробным числом (число с плавающей точкой)
  • boolean() — логическое («булево») поле — истина (true) или ложь (false)
  • date() — поле даты
  • timestamp() — поле «отпечатка времени», так называемый «Unix timestamp»
  • text() — текстовое поле без ограничения по длине
  • blob() — большой двоичный объект (BLOB)

Перед тем как на основе существующих миграций создавать таблицы, давайте создадим таблицу migrations, в которой laravel будет хранить данные о самих миграциях:

// ВНИМАНИЕ!Данну команду необходимо выполнять, если у нас еще не создана таблица migrations
php artisan migrate:install

В результате мы должны увидеть

“Migration table created successfully.”

Теперь, когда таблица создана, мы можем выполнить саму миграцию:

php artisan migrate

Чтобы удалить таблицу, можно выполнить команду rollback

php artisan migrate:rollback

3 .3 .4 Первоначальные данные

Для загрузки первоначальных данных имеется artisan-команда

php artisan make:seeder UserTableSeeder

Класс Seeder содержит только один метод по умолчанию run().

use Illuminate\Database\Seeder;

use Illuminate\Database\Eloquent\Model;

class DatabaseSeeder extends Seeder

{

    /**

     * Run the database seeds.

     *

     * @return void

     */

    public function run()

    {

        DB::table('users')->insert([

            'name' => str_random(10),

            'email' => str_random(10).'@gmail.com',

            'password' => bcrypt('secret'),

        ]);

    }

}

После того, как данные для загрузки подготовлены, нужно выполнить artisan-команду db:seed

php artisan db:seed// все классы

php artisan db:seed --class=UserTableSeeder // только указанный класс

Откатить данные можно с помощью команды rolback

php artisan migrate:refresh--seed

Можно использовать вспомогательные классы, в которых будет прописана логика загруки данных, загружаются они с помощью метода call().

public function run(){

 Model::unguard();

 $this->call(UserTableSeeder::class);

 $this->call(PostsTableSeeder::class);

 $this->call(CommentsTableSeeder::class);

}

3 .3 .5 Модели

Модель - это класс, ассоциированный с именем таблицы из базы данных. Причем, по-умолчанию, имя класса модели равняется имени таблицы из базы из которой убирается окончание “s”. Таким образом, модель Product ассоциирована с таблицей products.

Запросы в таблицу из модели формируются с помощью рассмотреннего ранее построителя запросов.

Для создания моделей у artisan имеется специальная команда.

php artisan make:model Flight

Если модель связывана с таблицей, а миграции создают таблицы, то логично было бы предположить, что имеется специальная комадна связывающая модель с миграцией. Действительно, у artisan имеется специальная команда, создающая сразу два файла: файл модели и файл миграции.

php artisan make:model Flight --migration
//или
php artisan make:model Flight -m

Не важно, какой командой мы воспользовались, в папке models проекта появится файл Flight.php со следующим содержимым:

namespace App;
use Illuminate\Database\Eloquent\Model;
class Flight extends Model
{
    //
}

В отличии от контроллера, который без методов бесполезен, такой пустой класс модели, мы уже можем использовать. Через данный класс модели, мы можем использовать методы конструктора запросов.

В Laravel имеется встроенная модель User, которая нас связывает с таблице users (используемая в авторизации).

Рассмотрим основные CRUD-операции (Create, Read, Update и Delete).

Извлечение данных

Как только модель определена, у вас всё готово для того, чтобы можно было выбирать и создавать записи. Обратите внимание, что вам нужно создать в этой таблице поля updated_at и created_at. Если вы не хотите, чтобы они были автоматически используемы, установите свойство $timestamps класса модели в false.

Получение всез записей модели

$users = User::all();

Получение записи по первичному ключу:

$user = User::find(1);
var_dump($user->name);

Метод find может принимать входящим параметром массив:

$user = User::find([1,2,4]);

Для извлечения данных по другим полям можно воспользоваться методом where.

 $user = User::where(‘email’,$email)->get();

При этом конечный метод get возвращает массив объектов, для вывода которых в последствии необходимо использовать foreach.

Метод where() может использоваться с тремя входящими параметрами, тогда вторым входящим параметром передается ключевой символ =, !=, >, <, <> или ключевое слово LIKE.

$user = User::where(‘name’,’!=’,$name)->get()

Метод where можно использовать в запросе несколько раз подряд, тогда последующие where работают как AND WHERE.

Множественный вызов метода where:

$user = Tovar::where(‘cat_id’,1)->where(‘showhide’,’show’)->get()

Существует еще один полезный метод, связанный с подзапросом where – это whereIn. WhereIn вторым (или трейтим) входящим параметром принимает массив значений, по которым необходимо формировать запрос.

$models = Model::whereIn('id', [1, 2, 3])->get();

Все методы, доступные в конструкторе запросов, также доступны в запросах с использованием моделей.

Иногда вам нужно возбудить исключение, если определённое значение не найдено, что позволит вам его отловить в обработчике App::error() и вывести страницу 404 («Не найдено»).

$model = User::findOrFail(1);
$model = User::where('votes', '>', 100)->firstOrFail();

Вставка

Данные через модель могут быть вставлены двумя способами:

  • Множественная вставка
  • Одиночная вставка

Пример одиночной вставки:

$user = new User; // создаем объект
$user->name = 'Джон'; // данные
$user->save(); // сохранение

Пример множественной вставки:

User::create(['name'=>'Джон']);

Считается, что способ множественной вставки менее безопасен одиночной. Поэтому для реализации множественной вставки, нужно специальное разрешение модели. В модели нужно объявить свойство $fillable, содержащее массив элементов полей, разрешенных для вставки.

$fillable = ['name'];

Обновление

Обновление данных по id:

$user = User::find(1);
$user->email = 'alex@ya.ru';
$user->save();

Удаление

Удаление данных по id

$user = User::find(1);
$user->delete();

Заготовки запросов

Заготовки позволяют повторно использовать логику запросов в моделях. Для создания заготовки просто начните имя метода со scope:

Создание заготовок запроса

class User extends Model {

  public function scopePopular($query)
  {
    return $query->where('votes', '>', 100);
  }

  public function scopeWomen($query)
  {
    return $query->whereGender('W');
  }

}

Использование заготовок запросов:

$users = User::popular()->women()->orderBy('created_at')->get();

Можно также использовать заготовки запросов с входящими параметрами:

class User extends Model {

  public function scopeOfType($query, $type)
  {
    return $query->whereType($type);
  }
}

Вызов заготовок запросов с входящими параметрами:

 $users = User::ofType('member')->get();

3 .3 .5 .1 Связии моделей

Таблицы базы данных часто связаны друг с другом. Например, пост в блоге может содержать много комментариев или заказ может быть связан с пользователем, который его разместил. Eloquent упрощает управление этими отношениями и работу с ними, а также поддерживает множество общих отношений:

  • Один к одному hasOne
  • Один ко многим hasMany
  • Принадлежность belongsTo
  • Многие ко многим belongsToMany
  • Многие через отношение hasMany(throught)
  • Полиморфные отношения

3 .3 .5 .2 Содержит один hasOne

Примеры связующих таблиц: страна - флаг, страна - столица, пользовать - аккаунт... Т.е. текущая модель может быть связана только с одной другой моделью.

Связь осуществляется с помощью метода hasOne()

...
class User extends Model {
 public function account(){
  return hasOne(Account::class);
 }
}

Для вызова связи в классах или шаблонах приложения, достаточно обратиться к этому методу: $obj->account(), или свойству $obj->account, которые возвращают объект связующей модели.

BelongsToMany

3 .3 .5 .3 Содержит много hasMany

Один пользователь может содержать множество телефонов. Тогда следует использовать связь hasMany(). Другими примерами данного отношения «один ко многим» является статья в блоге, которая имеет «много» комментариев или мама, имеющая детей. Мы можем смоделировать это отношение таким образом:

 class User extends Model{
  public function phones()
  {
    return $this->hasMany('Phone');
  }
 }

Теперь мы можем получить все телефоны пользователя с помощью динамического свойства:

$phones = User::find(1)->phones;

В этом случае, мы получили массив телефонов, и для просмотра всех телефонов, нужно пройтись по элементам массива.

 foreach($phones as $one){
  echo $one->body;
  echo '\n';
 }

Если нужно добавить ограничения на получаемые телефоны, можно вызвать метод phones() и продолжить добавлять условия:

$phone = User::find(1)->phones()->where('title', '=', 'foo')->first();

Теперь мы получили один телефон, который сразу можно вывести на экран:

 $phone->body

Рассмотрим еще один пример, который выявит недостатки связи hasMany. Предположим, у нас есть модель Book и модель Author. Book принадлежит (связь belongsTo) Author. И наоборот, Author содержит (связь hasMany) множество Book.

Определение связи в модели Book:

class Book extends Model {
    public function author()
{
        return $this->belongsTo('Author');
    }
}

Получить все названия книг можно следующим образом:

foreach (Book::all() as $book)
{
    echo $book->name;
}

Мы обошлись всего одним заросом. Даже если будет 100 книг, это всё-равно один запрос:

SELECT * FROM books

Однако, если к выводу названия книги добавим автора:

foreach (Book::all() as $book)
{
    echo $book->name;
    echo $book->author->name;
}

То, получаем 101 запрос. Первым запросом получаем все книги, а потом в цикле делаем запрос на получение автора книги.

SELECT * FROM books;
SELECT * FROM authors WHERE id = 1;
SELECT * FROM authors WHERE id = 2;
SELECT * FROM authors WHERE id = 3;
...

Это очень не рационально. И чтобы сократить нагрузку на сервер бызы данных, можно воспользоваться активной загрузкой.

Активная загрузка

Акнивная загрузка подразумевает подключение связующей модели с помощью метода with.

foreach (Book::with('author')->get() as $book)
{
    echo $book->author->name;
}

Таким образом будут выполнены всего два запроса:

 SELECT * FROM books;
 SELECT * FROM authors WHERE id IN(1,2,3);

Конечно, вы можете загрузить несколько отношений одновременно:

$books = Book::with('author', 'publisher')->get();

Разумное использование активной загрузки поможет сильно повысить производительность приложения.

3 .3 .6 Фабрики моделей

Для проведения теста иногда приходится заполнить базу данных, причём данные должны выглядеть более-менее как настоящие, а не просто набором букв. Данные можно вводить и в ручную, но это пойдёт, пока их мало. А в противном случае лучше воспользоваться специальным инструментом для заполнения тест данными — фабрикой моделей.

Хоть и при помощи сидов можно создать несколько записей, но фабрика моделей генерирует множество.

Файлы фабрик моделей хранятся в папке database/factories, и там уже есть один готовый файл UserFactory.php — фабрика для модели User. В Laravel 8 вызываемый метод definition() выглядит так:

    public function definition()
    {
        return [
            'name' => $this->faker->name,
            'email' => $this->faker->unique()->safeEmail,
            'email_verified_at' => now(),
            'password' => '$2y$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi', // password
            'remember_token' => Str::random(10),
        ];
    }

Из сидера фабрику можем вызвать так:

    public function run()
    {
        \App\Models\User::factory(10)->create();
    }

Далее запускаем сидер:

php artisan db:seed

Стоит так же отметить, что запускать фабрики мы можем не только из сидов, но и из других мест приложения. Например, из консоли. Для этого, сперва надо войти в tinker:

php artisan tinker

После полученного приглашения, можно выполнить фабрику:

\App\Models\User::factory(10)->create();

3 .4 Ресурсы в Laravel

При создании API может потребоваться слой преобразования, находящийся между моделями Eloquent и ответами JSON, которые возвращаются фронтенд части  web-приложения. Например, бывает необходимо отображать определенные атрибуты только для некоторых пользователей, а не для всех, или, кроме атрибутов, отображать отношения моделей. Классы ресурсов Eloquent позволяют легко и выразительно преобразовывать модели и коллекции моделей в JSON.

Конечно, всегда можно преобразовать модели или коллекции Eloquent в JSON, используя их методы toJson; однако ресурсы Eloquent обеспечивают более детальный и надежный контроль над выводом ответов.

 

3 .4 .1 Создание ресурса

Ресурсы расширяют класс Illuminate\Http\Resources\Json\JsonResource. Чтобы сгенерировать новый ресурс, можно использовать artisan-команду make:resource. Эта команда поместит новый класс ресурса в каталог app/Http/Resources приложения:

php artisan make:resource UserResource

3 .4 .2 Использование ресурса

Класс ресурсов представляет собой единую модель, которую необходимо преобразовать в структуру JSON. Например, вот простой класс ресурса UserResource:

namespace App\Http\Resources;

use Illuminate\Http\Resources\Json\JsonResource;

class UserResource extends JsonResource
{
    /**
     * Преобразовать ресурс в массив.
     *
     * @param  \Illuminate\Http\Request  $request
     * @return array
     */
    public function toArray($request)
    {
        return [
            'id' => $this->id,
            'name' => $this->name,
            'email' => $this->email,
            'created_at' => $this->created_at,
            'updated_at' => $this->updated_at,
        ];
    }
}

Каждый класс ресурсов определяет метод toArray, возвращающий массив атрибутов, которые должны быть преобразованы в JSON, когда ресурс возвращается в качестве ответа из метода маршрута или контроллера.

Мы можем получить доступ к свойствам модели непосредственно из переменной $this. Это связано с тем, что класс ресурсов автоматически возвращает свойства и методы к базовой модели. Как только ресурс определен, он может быть возвращен из маршрута или контроллера. Ресурс принимает основной экземпляр модели через свой конструктор:

use App\Http\Resources\UserResource;
use App\Models\User;

Route::get('/user/{id}', function ($id) {
    return new UserResource(User::findOrFail($id));
});

Чтобы вернуть коллекцию ресурса или ответ с постраничной разбивкой, то необходимо использовать метод collection класса ресурса, при создании экземпляра ресурса в маршруте или контроллере. Пример:

use App\Http\Resources\UserResource;
use App\Models\User;

Route::get('/users', function () {
    return UserResource::collection(User::all());
});

3 .4 .3 Ответы в формате JSON

В Laravel, чтобы отправить клиенту данные в формате JSON можно любым из перечисленных способов:

  • Из экшна (метода) контроллера вернуть результат запроса с методом toJson(<параметры кодирования>)

    Этот метод можно вызвать как для коллекции объектов модели:

     public function index(){
       return User::all()->toJson(JSON_UNESCAPED_UNICODE);
     }
    

    Так и для отдельного объекта модели:

     public function show(User $user){
       return $user->toJson(JSON_UNESCAPED_UNICODE);
     }
    

    При вызове метода toJson() можно указать параметры кодирования. Так, использованная в приведенных примерах опция кодированая JSON_UNESCAPED_UNICODE указывает не преобазовывать многобайтные символы (например, символы кириллицы) в их коды.

  • Вернуть из экшна контроллера результат вызова метода toArray(). Он полностью аналогичен методу toJson(), но без параметров кодирования

       // для отдельного объекта модели
       return $user->toArray();
       // для коллекции объектов модели
       return User::all()->toArray();
    
  • Если не требуется извлекать связные записи, то можно использовать метод attributesToArray()

        return $user->attributesToArray();
       
  • Вернуть из экшна контроллера результат приведения объекта модели или коллекции к строковому типу

       return (string) User::all();
    
  • Вернуть из эшкна контроллера объект модели или коллекции:

      public function show(User $user){
       return $user
      }
    
  • Если необходимо отправить клиенту закодированные в формат JSON произвольные данные, то можно воспользоваться инструментом json() класса ResponseFactory:

     public function show(User $user){
      return response()->json($user);
     }
    
  • Наконец, для реализации более сложных ответов, можно воспользоваться ресурсными классами. Далее сосредоточимся на ответах с помощью ресурсных классов.

3 .4 .4 Коллекции ресурсов

Помимо создания ресурсов, преобразующих отдельные модели, имеется возможность создавать ресурсы, отвечающие за преобразование коллекций моделей. Это позволяет ответам JSON включать ссылки и другую метаинформацию, имеющую отношение ко всей коллекции конкретного ресурса.

Чтобы сгенерировать новую коллекцию ресурса, нужно использовать флаг --collection при создании ресурса, или включить слово Collection в имя ресурса при его создании. Это укажет Laravel, что он должен создать коллекцию ресурса. Коллекции ресурса расширяют класс Illuminate\Http\Resources\Json\ResourceCollection:

php artisan make:resource User --collection

php artisan make:resource UserCollection

После создания класса коллекции ресурса, можно легко определить любые метаданные, которые должны быть включены в ответ:

namespace App\Http\Resources;

use Illuminate\Http\Resources\Json\ResourceCollection;

class UserCollection extends ResourceCollection
{
    /**
     * Преобразовать коллекцию ресурса в массив.
     *
     * @param  \Illuminate\Http\Request  $request
     * @return array
     */
    public function toArray($request)
    {
        return [
            'data' => $this->collection,
            'links' => [
                'self' => 'link-value',
            ],
        ];
    }
}

После определения коллекции ресурса, ее можно вернуть из маршрута или контроллера:

use App\Http\Resources\UserCollection;
use App\Models\User;

Route::get('/users', function () {
    return new UserCollection(User::all());
});

3 .4 .5 Структуры JSON-объектов, генерируемые ресурсными классами

В JSON-объекты можно заносить:

  • Поля объекта модели. Причём, свойствам, которые хранят значения полей, можно задавать любые имена.
     public function toArray() {
      return [
       'key' => $this->id,
       'title'=> $this->name,
       ...
      ];
     }
    
  • Любые произвольные значения.
     return [
      ...,
      'product_counts' => $this->products()->count()
     ];
    
     return [
      ...,
      'powered_by' => 'Laravel'
     ];
    
  • Произвольные значения, получаемые в результате вычислений по условию.

    Для вычислений по условию можно воспользоваться специальными ресурсными методами: when, mergeWhen:

     return [
      'count' => $this->when($this->products()->exists(), $this->products()->count()),
      'secret' => $this->when(Auth::user()->isAdmin(), 'secret-value'),
     ];
    

    Вместо выводимого значения (второй входящий параметр метода when), можно использовать анонимную функцию:

     return [
      ...,
      'count' => $this->when($this->products()->exists(), function(){
       return $this->products()->count();
     })
     ];
    
    Если есть несколько атрибутов, которые должны быть включены в ответ ресурса только на основе одного и того же условия, то можно использовать метод mergeWhen для включения атрибутов в ответ только тогда, когда данное условие истинно:
    public function toArray($request)
    {
        return [
            'id' => $this->id,
            'name' => $this->name,
            'email' => $this->email,
            $this->mergeWhen(Auth::user()->isAdmin(), [
                'first-secret' => 'value',
                'second-secret' => 'value',
            ]),
            'created_at' => $this->created_at,
            'updated_at' => $this->updated_at,
        ];
    }
    
  • Связанные записи - ввиде объектов соответствующих ресурсов:
     return [
      ...
      'parent' => new ProductResource($this->parent)
     ];
    
  • Коллекции связных записейм - ввдие объектов ресурсных коллекций.
     return [
      ...
      'products' => new ProductResourceCollection($this->products)
     ];
    
  • Значения из полей записи связующей таблицы.
    use App\Http\Resources\PostResource;
    
    /**
     * Transform the resource into an array.
     *
     * @param  \Illuminate\Http\Request  $request
     * @return array
     */
    public function toArray($request)
    {
        return [
            'id' => $this->id,
            'name' => $this->name,
            'email' => $this->email,
            'posts' => PostResource::collection($this->posts),
            'created_at' => $this->created_at,
            'updated_at' => $this->updated_at,
        ];
    }
    
    В том числе в связях "многие-со-многими":
     return [
      ...
      'products' => $this->pivot->products
     ];
    
  • Значения из полей записи связующей таблицы по условию. Для этого можно воспользоваться методом whenLoaded
    use App\Http\Resources\PostResource;
    
    /**
     * Transform the resource into an array.
     *
     * @param  \Illuminate\Http\Request  $request
     * @return array
     */
    public function toArray($request)
    {
        return [
            'id' => $this->id,
            'name' => $this->name,
            'email' => $this->email,
            'posts' => PostResource::collection($this->whenLoaded('posts')),
            'created_at' => $this->created_at,
            'updated_at' => $this->updated_at,
        ];
    }
    
    В дополнение к условному включению сведений об отношениях в ответы вашего ресурса вы можете условно включить данные из промежуточных таблиц отношений многие ко многим, используя метод whenPivotLoaded. Метод whenPivotLoaded принимает имя сводной таблицы в качестве первого аргумента. Вторым аргументом должно быть замыкание, которое возвращает значение, которое будет возвращено, если в модели доступна сводная информация:
    **
     * Transform the resource into an array.
     *
     * @param  \Illuminate\Http\Request  $request
     * @return array
     */
    public function toArray($request)
    {
        return [
            'id' => $this->id,
            'name' => $this->name,
            'expires_at' => $this->whenPivotLoaded('role_user', function () {
                return $this->pivot->expires_at;
            }),
        ];
    }
    

3 .4 .6 Постраничная навигация в ресурсных коллекциях

Вместо коллекции записей в ресурсный класс можно передать объект пагинатора:

 public function index(){
  return new ProductResourceCollection(Product::paginate(10));
 }

Тогда JSON-объект сгенерированный ресурсным классом будет содержать следующие свойства:

  • data - массив с записями выбранной части;
  • links - значения пагинатора. Содрежит вложенный объект со следующими свойствами:
    • first - ссылка на первую часть пагинатора;
    • last - ссылка на последнюю часть пагинатора;
    • prev - ссылка на предыдущую часть пагинатора, или null если это первая часть;
    • next - ссылка на следующую часть пагинатора, или null, если это послденяя часть.
  • meta - данные о выводимой части пагинатора, со следующими свойствами
    • current_page - порядковый номер текущей части, начиная с 1;
    • last_page - порядковый номер последней части пагинатора;
    • per_page - предельное количество записей, входящих в выводимую часть;
    • from - порядковый номер первой записи, входящей в текущую выводимую часть пагинатора;
    • to - порядковый номер последней части, входящей в текущую выводимую часть пагинатора;
    • total - общее количество записей в коллекции;
    • path - текущая ссылка.

Класс ресурсной коллекции содержит еще два метода:

  • withQuery(<массив GET-параметров>) - позволяет добавить к ссылке дополнительные GET-параметры. Пример
 public function index(){
  return new ProductResourceCollection(Product::paginate(10))->withQuery(['search'=>'Laravel']);
 }
  • preserveQuery() - указывает включить в ссылку все GET-параметры, что присутствуют в запросе.

3 .4 .7 Дополнительные данные в результирующем JSON-объекте

К результирующему JSON-объекту можно добавить дополнительные данные, привязав их к ответу, или к заголовку.

  • Дополительные данные в ответе. Для этого можно воспользоваться специальным методом additional.
 public function index(Product $product){
  return new ProductResource($product)->additional(['powered'=>'Laravel']);
 }
  • Все ресурсные классы поддерживают метод response(), через который можно вызвать метод header() с дополнительными данными в заголовке
 public function index(Product $product){
  return new ProductResource($product)->response()->header('X-Data-Kind', 'Product');
 }

3 .5 Авторизация в API

Sanctum Laravel - это авторизация для API. Используется в бэкенд-приложениях.

Sanctum решает две проблемы: формирование API токена и аутентификации в приложении.

Установка и настройка

Сперва необходимо установить плагин с помощью  composer

composer require laravel/sanctum

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

php artisan vendor:publish --provider="Laravel\Sanctum\SanctumServiceProvider"

Sanctum создаст нужную миграцию для хранения токенов. Миграцию необходимо выполнить:

php artisan migrate

Далее находим файл App\Http\Kernel.php и заменяем значение api

'api' => [
    \Laravel\Sanctum\Http\Middleware\EnsureFrontendRequestsAreStateful::class,
    'throttle:api',
    \Illuminate\Routing\Middleware\SubstituteBindings::class,
],

Model User

Далее открываем модель User, и вносим сюда следующие изменения:

use Laravel\Sanctum\HasApiTokens;
 
class User extends Authenticatable
{
    use HasApiTokens, HasFactory, Notifiable;
}

Middleware

Теперь для защиты маршрутов в API мы можем использовать middleware auth:sanctum:


Route::middleware('auth:sanctum')->get('/user', function (Request $request) {
    return $request->user();
});

Данный middleware уже используется в файле routes/api.php.. Впоследствии перепишем его, создав группу для middleware auth:sanctum

Request UserRequest

Валидировать данные, необходимые для регистрации пользователя будем с помощью класса UserRequest. Сперва создадим его:

php artisan make:request UserRequest

Внесем необходимые изменения в класс:


namespace App\Http\Requests;

use Illuminate\Foundation\Http\FormRequest;

class UserRequest extends FormRequest
{
    /**
     * Determine if the user is authorized to make this request.
     *
     * @return bool
     */
    public function authorize()
    {
        return true;
    }

    /**
     * Get the validation rules that apply to the request.
     *
     * @return array
     */
    public function rules()
    {
        return [
            'name'=>'required|string',
            'email'=>'required|string|unique:users|email',
            'password'=>'required|confirmed'
        ];
    }
}

Таким образом, со стороны фронта нужно будет отправить следующие request-ы: name, email, password, password_confirmation

Controller AuthController

Создадим контроллер AuthController

php artisan make:controller AuthController

Добавим метод регистрации register

namespace App\Http\Controllers;

use Illuminate\Http\Request;
use App\Models\User;
use App\Http\Requests\UserRequest;
use Illuminate\Support\Facades\Hash;

class AuthController extends Controller
{
    public function register(UserRequest $r){
        $r['password'] = Hash::make($r->password);
        $user = User::create($r->all());
        $token = $user->createToken('myapptoken')->plainTextToken;
        $response = [
            'user'=>$user,
            'token' => $token
        ];
        return response()->json($response);
    }
}

Маршрутизация

Для настройки маршрутов сперва в файл routes/api.php подключим namespace к папке с контроллерами.

use App\Http\Controllers;

Теперь можем прописать нужные маршруты, разбив наши маршуты пока на две группы - открытые (public) и защищенные (protected) маршруты:


//public routes
Route::post('register', [Controllers\AuthController::class, 'register']);
    
//protected routes
Route::group(['middleware'=>['auth:sanctum']], function(){

});

Postmen

Для тестирования запросов на бэк воспользуемся postmen. В папке с приложением создадим папку с коллекцией Auth и добавим туда нужные запросы. Обратие внимание на Headers. В заголовках должен быть отправлен Accept со значением application/json. После выставления заголовков, можно переключиться на вкладку params и там заполнить нужные для авторизации поля. После нажатия на кнопку send, при успешной авторизации мы увидим в ответе объект пользователя и token:

После того, как мы получили token, мы можем использовать его для защищённых запросов. Скопируем значение токена, и добавим его в значение Bearer Token вкладки Auth

Login и logout

Добавим в контроллер AuthController экшн входа - login.

    public function login(Request $request){
        $user = User::where('email', $request->email)->first();
        if(!$user || !Hash::check($request->password, $user->password)){
            return response()->json([
                'message' => 'bad credits'
            ]);
        }
        $token = $user->createToken('myapptoken')->plainTextToken;
        $response = [
            'user' => $user,
            'token' => $token
        ];
        return response()->json($response);
    }

Экшн выхода logout:

    public function logout(Request $request)
    {
        auth()->user()->tokens()->delete();
        return response()->json(['message'=>'Logged out']);
    }

Заключение

Список использованных источников

Приложения