Как использовать Fauna с GraphQL и PHP

Fauna — это гибкая, распределенная документально-реляционная база данных, предоставляемая в виде безопасного и масштабируемого облачного API со встроенной поддержкой GraphQL. Fauna использует парадигму многомодельной базы данных, то есть обладает гибкостью базы данных NoSQL и реляционными возможностями запросов и транзакций базы данных SQL. Fauna была создана с учетом современных приложений и хорошо подходит для облачных приложений, которым нужен надежный, ACID-совместимый, облачный сервис для поддержки аудитории в разных регионах.

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

  • Возможность получения вложенных данных за один вызов
  • Оптимизированное использование полосы пропускания
  • Минимальные административные накладные расходы
  • Низкая задержка транзакций с быстрым чтением/записью — независимо от того, где работает ваш клиент
  • Мощные запросы к NoSQL, включая возможность создания объединений и программирования бизнес-логики с помощью очень выразительного языка запросов.
  • Расширенное индексирование
  • Многопользовательская поддержка,
  • Уровень абстракции реализации базы данных через GraphQL

Эти возможности делают Fauna хорошо подходящей для работы с большими объемами структурированных и неструктурированных данных.

Это руководство покажет вам, как можно использовать Fauna и PHP для создания мощных приложений.

Вы создадите простое приложение для управления проектами, которое использует API GraphQL от Fauna. В этой статье вы узнаете больше о том, как Fauna работает с GraphQL и PHP. Вы пройдете через процесс создания простого приложения для управления проектами, которое использует GraphQL и Fauna для операций CRUD. Вот как будет выглядеть ваше готовое приложение:

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

Содержание
  1. Создание с помощью Fauna, GraphQL и PHP
  2. Необходимые условия
  3. Создание базы данных Fauna
  4. Загрузка схемы GraphQL
  5. Настройка клиента
  6. Структура проекта
  7. Создайте каталог проекта
  8. Настройка виртуального хоста локально
  9. В Windows
  10. На Mac
  11. Для обеих операционных систем
  12. Установите необходимые зависимости
  13. Создайте необходимые папки и файлы
  14. Маршрутизация обработок
  15. Работа с представлениями
  16. Создание классов контроллеров
  17. Работа с маршрутами с помощью классов контроллера
  18. Обновление файлов шаблонов и CSS
  19. Настройка клиента GraphQL
  20. Создание ключа доступа к базе данных Fauna
  21. Пользовательские резольверы и определяемые пользователем функции (UDF)
  22. Запрос данных
  23. Обновление класса клиента GraphQL
  24. Обновление классов контроллеров
  25. Аутентификация и управление сессиями
  26. Запуск сессии
  27. Зарегистрироваться
  28. Вход в систему
  29. Формы и обновления
  30. Главная страница
  31. Обновление проекта
  32. Заключение

Создание с помощью Fauna, GraphQL и PHP

В этом разделе вы узнаете, как простой менеджер проектов под названием Faproman построен с использованием Fauna, GraphQL и PHP. Для простоты здесь используется ванильный PHP. Исходный код приложения можно найти здесь.

Необходимые условия

  • Учетная запись в Fauna, которую можно создать здесь.
  • Базовые знания GraphQL и объектно-ориентированного PHP.
  • Установленные PHP и Composer. XAMPP — это дистрибутив Apache с открытым исходным кодом, содержащий PHP. Composer — это менеджер зависимостей для PHP.

💡Посмотрите семинар Fauna для разработчиков GraphQL — самостоятельное изучение возможностей Fauna в области GraphQL.

Создание базы данных Fauna

Fauna предлагает возможность импортировать схемы GraphQL. Когда схема импортирована, Fauna:

  • Создает коллекции для типов данных в схеме, исключая встроенные типы.
  • Добавляет поля id документа (_id) и timestamp (ts) к каждому документу, который будет существовать в коллекции. Документы одного типа принадлежат одной коллекции.
  • Создает индексы для каждого именованного запроса с параметрами поиска, полученными из параметров полей для каждого отношения между типами.
  • Создает вспомогательные запросы и мутации для помощи в CRUD-операциях над созданными коллекциями.
  • Предоставляет конечную точку GraphQL и игровую площадку с вкладками Schema и Docs для просмотра списка возможных запросов и доступных типов.

Вы начнете с создания базы данных Fauna. Войдите в свою панель Fauna. Приборная панель для только что зарегистрированного пользователя выглядит следующим образом:

Нажмите на кнопку Создать базу данных.

В форме Create Database заполните поле Name именем базы данных (например, Faproman) и выберите группу регионов. Поле Region Group позволяет вам определить географический регион, в котором будут находиться ваши данные. Если вы не уверены, какой регион выбрать, выберите Classic (C).

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

Нажмите CREATE, и ваша новая база данных будет создана.

Загрузка схемы GraphQL

Прежде чем начнется взаимодействие GraphQL API с нашей базой данных, Fauna должна получить схему GraphQL. Сначала давайте определим структуру схемы, подходящую для данного приложения.

Создаваемое приложение для управления проектами имеет отношения «один-ко-многим», поскольку у одного пользователя может быть много проектов. Такие отношения могут быть представлены в схеме GraphQL двумя типами: источник (для пользователя) и цель (для проектов). Тип source будет иметь поле массива, указывающее на тип target, а target будет иметь поле без массива, указывающее обратно на source.

Ниже приведены типы источника (User) и цели (Project):

type User {
    username: String! @unique
    password: String!
    project: [Project!] @relation # points to Project
    create_timestamp: Long!
    # _id: Generated by Fauna as each document's unique identifier
    # _ts: Timestamp generated by Fauna upon object updating
}

type Project {
    name: String!
    description: String!
    owner: User! # points to User
    completed: Boolean
    create_timestamp: Long
    # _id: Generated by Fauna as each document's unique identifier
    # _ts: Timestamp generated by Fauna upon object updating
}
Войти в полноэкранный режим Выйти из полноэкранного режима

Директива @relation для поля project типа User помогает убедиться, что Фауна распознает это поле как поле, создающее отношение, а не как массив идентификаторов. Директива @unique для поля username типа User гарантирует, что два пользователя не могут иметь одинаковое имя пользователя.

Тип Query определен ниже:

type Query {
    findUserByUsername(username: String!): User @resolver(name: "findUserByUsername") # find a user by username
    findProjectsByUserId(owner_id: ID!): [Project] @resolver(name: "findProjectsByUserId") #find projects belonging to a user
}
Войти в полноэкранный режим Выйти из полноэкранного режима

Директива @resolver в полях запроса определяет пользовательскую функцию, которая будет использоваться для разрешения запросов. Поле name задает имя функции.

Тип Mutation определен ниже:

type Mutation {
    createNewProject(
        owner_id: ID!
        name: String!
        description: String!
        completed: Boolean,
        create_timestamp: Long
    ): Project @resolver(name: "createNewProject")

    createNewUser(
        username: String!
        password: String!
        create_timestamp: Long
    ): User @resolver(name: "createNewUser")

    deleteSingleProject(id: ID!): Project @resolver(name: "deleteSingleProject")

    updateSingleProject(
        project_id: ID!
        name: String!
        description: String!
        completed: Boolean,
        create_timestamp: Long
    ): Project @resolver(name: "updateSingleProject")

}
Войти в полноэкранный режим Выйти из полноэкранного режима

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

Создайте файл с именем schema.gql и вставьте в него типы источника, цели, запроса и мутации. Теперь схема готова к загрузке.

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

Нажмите на IMPORT SCHEMA, затем выберите файл schema.gql, который вы создали ранее. После успешного завершения загрузки вы увидите игровую площадку GraphQL, которую вы можете использовать для взаимодействия с GraphQL API вашей базы данных Fauna.

Обратите внимание, что помимо запросов и мутаций, указанных в схеме, Fauna автоматически создает основные CRUD-запросы и мутации для исходного (User) и целевого (Project) типов. Эти автоматически созданные запросы и мутации автоматически разрешаются Fauna, что означает, что вы можете сразу же начать их использовать. Для использования запросов и мутаций, указанных в файле схемы, должны быть созданы собственные резолверы — вы создадите их позже в этом руководстве.

Настройка клиента

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

Структура проекта

Ниже приведена файловая структура Faproman:

faproman                                             
├─ app                                               
│  ├─ controller                                     
│  │  ├─ ProjectController.php                       
│  │  └─ UserController.php                          
│  ├─ helper                                         
│  │  └─ View.php                                    
│  └─ lib                                            
│     └─ Fauna.php                                   
├─ public 
│  ├─ .htaccess
│  ├─ assets                                         
│  │  └─ style.css                                   
│  └─ index.php                                      
├─ temp                                              
│  ├─ 404.php                                        
│  ├─ footer.php                                     
│  ├─ header.php                                     
│  └─ nav.php                                                                     
├─ views                                             
│  ├─ edit.php                                       
│  ├─ home.php                                       
│  ├─ login.php                                      
│  └─ register.php                                   
├─ composer.json                                     
└─ composer.lock   
Вход в полноэкранный режим Выход из полноэкранного режима

Создайте каталог проекта

Найдите папку htdocs, или каталог, в котором ваш веб-сервер Apache ищет файлы для обслуживания по умолчанию, и создайте в этом каталоге папку faproman. Для XAMPP эта директория находится в /xampp/htdocs.

Создайте подпапку public внутри каталога faproman. Позже ваш сервер Apache будет перенастроен на обслуживание файлов из папки public специально для Faproman.

Настройка виртуального хоста локально

Виртуальный хост — это правило конфигурации сервера Apache, которое позволяет указать корень документа сайта (каталог или папку, содержащую файлы сайта). Виртуальный хост поможет вам настроить локальный домен, например faproman.test, который будет указывать на общую папку проекта при любом обращении к домену.

Чтобы настроить виртуальный хост на локальной машине, начните с обновления файла hosts вашей операционной системы. Файл hosts сопоставляет имена хостов с IP-адресами.

  • Для Windows OS найдите файл hosts по адресу C:/Windows/System32/drivers/etc/hosts.
  • Для Mac OS найдите файл hosts по адресу /etc/hosts.

Откройте файл в удобном для вас редакторе и добавьте в нижней части следующее:

127.0.0.1   localhost
127.0.0.1   faproman.test # for Faproman app
Войти в полноэкранный режим Выйти из полноэкранного режима

Обновите файл виртуальных хостов.

В Windows

В Windows OS (xampp) файл находится по адресу c:/xampp/apache/conf/extra/httpd-vhosts.conf.

Откройте файл в удобном для вас редакторе и добавьте внизу следующее:

<VirtualHost *:80>
    DocumentRoot "C:/xampp/htdocs"
    ServerName localhost
</VirtualHost>

<VirtualHost *:80>
    DocumentRoot "C:/xampp/htdocs/faproman/public" #serve the public folder
    ServerName faproman.test
</VirtualHost>
Войти в полноэкранный режим Выйти из полноэкранного режима

На Mac

На Mac OS (xampp) файл находится по адресу /Applications/XAMPP/xamppfiles/etc/extra/httpd-vhosts.conf.

Откройте файл в удобном для вас редакторе и добавьте в нижней части следующее:

<VirtualHost *:80>
    DocumentRoot "/Applications/XAMPP/xamppfiles/htdocs"
    ServerName localhost
    ServerAlias www.localhost
</VirtualHost>

<VirtualHost *:80>
    DocumentRoot "/Applications/XAMPP/xamppfiles/htdocs/faproman/public"
    ServerName faproman.test
    ServerAlias www.faproman.test
</VirtualHost>
Войти в полноэкранный режим Выйти из полноэкранного режима

Для обеих операционных систем

Далее создайте файл index.php внутри /htdocs/faproman/public и добавьте в него приведенный ниже код для вывода текста «Это тест»:

   <?php
   echo "This is a test";
Войти в полноэкранный режим Выход из полноэкранного режима

.
Запустите ваш сервер Apache через панель управления xampp.

Перейдите по адресу http://faproman.test или http://faproman.test:8080 в вашем браузере. Вы увидите текст, который вы только что вывели.

Установите необходимые зависимости

Создайте файл composer.json в корневом каталоге Faproman (/htdocs/faproman) и добавьте следующий код:

{
    "autoload": {
        "psr-4": {
            "App\": "app/"
        }
    }
}
Войти в полноэкранный режим Выйти из полноэкранного режима

Это гарантирует, что любое пространство имен, начинающееся с App, будет отображено на папку app/ в приложении.

Откройте терминал в каталоге faproman и выполните приведенную ниже команду для установки всех необходимых зависимостей:

composer require altorouter/altorouter gmostafa/php-graphql-client vlucas/phpdotenv
Войти в полноэкранный режим Выйти из полноэкранного режима

Это установит следующие пакеты:

  • altorouter/altorouter: Для управления маршрутами.
  • gmostafa/php-graphql-client: Для выполнения запросов к конечной точке GraphQL.
  • vlucas/phpdotenv: Для загрузки переменных окружения из файлов .env.

Создайте необходимые папки и файлы

Создайте все папки и файлы внутри папки faproman в соответствии с приведенной выше структурой проекта. Откройте папку faproman в вашем любимом редакторе кода.

Маршрутизация обработок

Конфигурационный файл .htaccess может быть использован для того, чтобы заставить серверы Apache перенаправлять запросы ко всем PHP-файлам в приложении на один PHP-файл.

Откройте файл /public/.htaccess и вставьте приведенную ниже конфигурацию:

RewriteEngine on
RewriteBase /
RewriteCond %{REQUEST_FILENAME} !-f
RewriteRule . index.php [L]
Войти в полноэкранный режим Выйти из полноэкранного режима

Это перенаправит запросы на файл index.php в директории public, что позволит вам лучше управлять маршрутизацией.

Откройте файл public/index.php и вставьте приведенный ниже код для настройки маршрутизации с помощью пакета altorouter/altorouter:

<?php
/**
 * Autoload classes
 */ 
require __DIR__ . '/../vendor/autoload.php';

$router = new AltoRouter();

// routes
$router->addRoutes(array());

/* Match the current request */
$match = $router->match();

// call closure or throw 404 status
if( is_array($match) && is_callable( $match['target'] ) ) {
    // call the action function and pass parameters
    call_user_func_array( $match['target'], $match['params'] ); 
} else {
    // no route was matched
    header("HTTP/1.0 404 Not Found");
    echo "404 | Not Found"
}
Вход в полноэкранный режим Выход из полноэкранного режима

Работа с представлениями

Откройте файл app/helper/View.php и введите следующий код для создания класса-помощника, который будет рендерить файлы представлений в директории view.

<?php

namespace AppHelper;

class View {

    private static $path = __DIR__ . '/../../views/';
    private static $tempPath = __DIR__ . '/../../temp/';

    public static function render(string $view, array $parameters = array(), string $pageTitle = 'Faproman') {
        // make page title available
        $pageTitle = $pageTitle;
        // extract the parameters into variables
        extract($parameters, EXTR_SKIP);
        require_once(self::$tempPath . 'header.php');
        require_once(self::$tempPath . 'nav.php');
        require_once(self::$path . $view);
        require_once(self::$tempPath . 'footer.php');
    }

    public static function render404() {
        $pageTitle = '404 | Not Found - Faproman';
        require_once(self::$tempPath . 'header.php');
        require_once(self::$tempPath . '404.php');
        require_once(self::$tempPath . 'footer.php');
    }

}

Вход в полноэкранный режим Выход из полноэкранного режима

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

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

  • UserController: обрабатывает все запросы, связанные с пользователями.
  • ProjectController: обрабатывает все запросы, связанные с проектом.

Откройте файл app/controller/UserController.php и создайте класс UserController, как показано ниже:

<?php

namespace AppController;
use AppHelperView;

class UserController {

    public function login() {
        View::render('login.php', array(), 'Login - Faproman');
    }

    // show register form
    public function register() {
        View::render('register.php', array(), 'Register - Faproman');
    }

    // logout user
    public function logout() {}

    // create new user
    public function create() {
        $errorMsgs = array();}

    // login user
    public function authenticate() {}

}
Вход в полноэкранный режим Выход из полноэкранного режима

Откройте файл app/controller/ProjectController.php и создайте класс ProjectController:

<?php

namespace AppController;
use AppHelperView;

class ProjectController {

    // home page
    public function index() {
        View::render('home.php', array(), 'Home Page');
    }

    // edit page
    public function edit(string $id) {
        View::render('edit.php', array('projectId' => $id), 'Edit Project');
    }

    // create new user
    public function create() {}

    // update a project
    public function update() {}

    // delete a project
    public function delete() {}

}
Вход в полноэкранный режим Выход из полноэкранного режима

Обратите внимание, как метод View::render() используется для рендеринга файлов представления.

Работа с маршрутами с помощью классов контроллера

Обновите файл public/index.php следующим образом:

<?php
/**
 * Autoload classes
 */ 
require __DIR__ . '/../vendor/autoload.php';

use AppControllerProjectController;
use AppControllerUserController;
use AppHelperView;

$router = new AltoRouter();

// routes
$router->addRoutes(array(
    array('GET','/', array(new ProjectController, 'index')),
    array('GET','/edit/[i:id]', array(new ProjectController, 'edit')),
    array('GET','/login', array(new UserController, 'login')),
    array('GET','/register', array(new UserController, 'register')),
    array('GET','/logout', array(new UserController, 'logout')),
    array('POST','/user/create', array(new UserController, 'create')),
    array('POST','/user/authenticate', array(new UserController, 'authenticate')),
    array('POST','/project/create', array(new ProjectController, 'create')),
    array('POST','/project/update', array(new ProjectController, 'update')),
    array('POST','/project/delete', array(new ProjectController, 'delete')),
));

/* Match the current request */
$match = $router->match();

// call closure or throw 404 status
if( is_array($match) && is_callable( $match['target'] ) ) {
    // call the action function and pass parameters
    call_user_func_array( $match['target'], $match['params'] ); 
} else {
    // no route was matched
    header("HTTP/1.0 404 Not Found");
    View::render404();
}
Войдите в полноэкранный режим Выйти из полноэкранного режима

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

Обновление файлов шаблонов и CSS

Откройте каждый из следующих файлов и добавьте в каждый из них соответствующий код.

Чтобы определить заголовок, отредактируйте temp/header.php:

  <!DOCTYPE html>
  <html>
  <head>
      <meta charset="utf-8">
      <meta http-equiv="X-UA-Compatible" content="IE=edge">
      <title><?php echo isset($pageTitle) ? $pageTitle : null; ?></title>
      <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/css/bootstrap.min.css" rel="stylesheet"
          integrity="sha384-EVSTQN3/azprG1Anm3QDgpJLIm9Nao0Yz1ztcQTwFspd3yD65VohhpuuCOmLASjC" crossorigin="anonymous">
      <link rel="stylesheet" href="/assets/style.css" />
      <style>
        body {
          padding-top: 56px;
        }
      </style>
  </head>
  <body>
Вход в полноэкранный режим Выйти из полноэкранного режима

Файл JavaScript Bootstrap добавляется непосредственно перед закрывающим </body>, чтобы компоненты bootstrap, требующие JavaScript, работали правильно.

Чтобы определить футер, отредактируйте temp/footer.php:

  <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/js/bootstrap.bundle.min.js" integrity="sha384-MrcW6ZMFYlzcLA8Nl+NtUVF0sA7MsXsP1UyJoMp4YLEuNSfAP+JcXn/tWtIaxVXM" crossorigin="anonymous"></script>
  </body>
  </html>
Вход в полноэкранный режим Выход из полноэкранного режима

Файл JavaScript Bootstrap добавляется непосредственно перед закрывающим </body>, чтобы компоненты bootstrap, требующие JavaScript, работали правильно.

Чтобы определить навигационную панель, отредактируйте temp/nav.php:

  <nav class="navbar navbar-expand-lg navbar-dark bg-dark fixed-top">
      <div class="container-fluid px-4">
          <a class="navbar-brand" href="/">Faproman</a>
          <button class="navbar-toggler" type="button" data-bs-toggle="collapse"
              data-bs-target="#navbarSupportedContent" aria-controls="navbarSupportedContent" aria-expanded="false"
              aria-label="Toggle navigation">
              <span class="navbar-toggler-icon"></span>
          </button>
          <div class="collapse navbar-collapse" id="navbarSupportedContent">
              <ul class="navbar-nav me-auto mb-2 mb-lg-0">
              </ul>
              <div class="d-flex align-items-center">
                  <?php if (isset($_SESSION['logged_in_user'])) { ?>
                      <span class="text-white me-3"><i class="bi bi-person-circle"></i> <?php echo $_SESSION['logged_in_user']->username ?></span>
                      <a href="/logout" class="btn btn-outline-light">Logout</a>
                  <?php } else { ?>
                      <a href="/login" class="btn btn-outline-light">Login</a>
                  <?php } ?>
              </div>
          </div>
      </div>
  </nav>
Войдите в полноэкранный режим Выход из полноэкранного режима

Чтобы определить страницу 404, отредактируйте temp/404.php:

  <div class="page-404">
      404 | Not Found
  </div>
Войдите в полноэкранный режим Выйти из полноэкранного режима

Чтобы импортировать шрифт и иконки, а также добавить дополнительные стили, отредактируйте public/assets/style.css:

  @import url('https://fonts.googleapis.com/css2?family=Nunito&display=swap');
  @import url("https://cdn.jsdelivr.net/npm/bootstrap-icons@1.8.3/font/bootstrap-icons.css");

  body {
      font-family: Nunito, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif !important;  
  }

  .form-error-box {
      color: #a94442;
    background-color: #f2dede;
    border: 1px solid #ebccd1;
    border-radius: 4px;
    font-size: 11px;
    padding: 6px;
    margin: 10px 0;
    -webkit-border-radius: 4px;
    -moz-border-radius: 4px;
    -ms-border-radius: 4px;
    -o-border-radius: 4px;
  }

  .page-404 {
    position: fixed;
    top: 0;
    left: 0;
    height: 100vh;
    width: 100vw;
    display: flex;
    justify-content: center;
    align-items: center;
    font-size: 35px;
  }
Войдите в полноэкранный режим Выйти из полноэкранного режима

Настройка клиента GraphQL

Теперь вы создадите класс Fauna, который будет обрабатывать все взаимодействия с вашей базой данных. Откройте файл app/lib/Fauna.php и вставьте приведенный ниже текст:

<?php

namespace AppLib;

use DotenvDotenv;
use GraphQLClient;

// load env variables to $_ENV super global
$dotenv = Dotenv::createImmutable(__DIR__ . '/../../');
$dotenv->load();

define('CLIENT_SECRET', $_ENV['SECRET_KEY']);
define('FAUNA_GRAPHQL_BASE_URL', 'https://graphql.fauna.com/graphql');

class Fauna {
    public static function getClient(): Client {
        return new Client(
            FAUNA_GRAPHQL_BASE_URL,
            ['Authorization' => 'Bearer ' . CLIENT_SECRET]
        );
    }

    public static function createNewUser(string $username, string $password, int $create_timestamp) {}

    public static function getUserByUsername(string $username) {}

    public static function createNewProject(string $userId, string $name, string $description, bool $completed) {}

    public static function getProjectsByUser(string $id) {}

    public static function updateExistingProject(string $projectId, string $name, string $description, bool $completed) {} 

    public static function deleteExistingProject(string $projectId) {}

    public static function getSingleProjectByUser(string $projectId) {}
Войти в полноэкранный режим Выйти из полноэкранного режима

Метод $dotenv->load() загружает все переменные окружения из файла .env в суперглобалы $_ENV и $_SERVER. Ключи из вашей базы данных Fauna должны храниться в файле .env в корневом каталоге faproman, что сделает ключи доступными в вышеупомянутых суперглобалах.

Метод Fauna::getClient() возвращает новый настроенный экземпляр вашего клиента GraphQL.

Создание ключа доступа к базе данных Fauna

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

Перейдите на панель управления базой данных и нажмите на [ Безопасность ] на боковой панели.

Нажмите на Новый ключ.

В форме создания ключа выберите текущую базу данных в качестве базы данных, Server в качестве роли и faproman_app в качестве имени ключа.

Обратите внимание, что новая база данных по умолчанию имеет две роли: Admin и Server. Вы также можете создать собственные роли с желаемыми привилегиями. Роль Server выбрана потому, что все взаимодействие с базой данных будет осуществляться с помощью PHP — языка, используемого на стороне сервера.

Нажмите на кнопку Сохранить, чтобы сохранить ключ. Скопируйте секрет ключа и сохраните его в безопасном месте. Кроме того, вставьте секрет ключа в файл .env в корневом каталоге с именем переменной SECRET_KEY.

Теперь ваше приложение готово к обращению к Fauna.

Пользовательские резольверы и определяемые пользователем функции (UDF)

Напомним, что в схеме для Faproman, рассмотренной ранее, директива @resolver использовалась для указания определяемых пользователем функций, которые будут использоваться в качестве пользовательских резольверов для разрешения запросов или мутаций. Определяемые пользователем функции, которые указываются директивой @resolver, должны быть созданы для того, чтобы пользовательские резольверы работали при запросе полей.

Определяемые пользователем функции могут быть созданы либо через встроенную оболочку Fauna, либо через форму создания функций. Они определяются функциями, предоставляемыми языком запросов Fauna Query Language (FQL).

Прежде чем продолжить, выполните следующие действия в оболочке для создания индексов project_ower_idx и user_username_idx.

CreateIndex({
  name: "project_owner_idx",
  source: Collection("Project"),
  terms: [{ field: ["data", "owner"] }],
  serialized: true
})

CreateIndex({
  name: "user_username_idx",
  source: Collection("User"),
  terms: [{ field: ["data", "username"] }],
  serialized: true
})
Вход в полноэкранный режим Выход из полноэкранного режима

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

Примечание: Если какая-либо из приведенных ниже функций уже существует, нажмите на [ Function ] на панели Fauna, нажмите на эту функцию и нажмите на [ Delete ] под определением функции, чтобы удалить функцию. Выполните команду в оболочке, чтобы воссоздать функцию.

  CreateFunction({
    name: "findUserByUsername",
    body: Query(
        Lambda(
          ["username"],
          Get(
            Select(
              ["data", 0],
              Paginate(Match(Index("user_username_idx"), Var("username")))
            )
          )
        )
      )
  })
Вход в полноэкранный режим Выход из полноэкранного режима
  CreateFunction({
    name: "findProjectsByUserId",
    body: Query(
        Lambda(
          ["owner_id"],
          Select(
            "data",
            Map(
              Paginate(
                Reverse(
                  Match(
                    Index("project_owner_by_user"),
                    Ref(Collection("User"), Var("owner_id"))
                  )
                )
              ),
              Lambda(["ref"], Get(Var("ref")))
            )
          )
        )
      )
  })
Войти в полноэкранный режим Выход из полноэкранного режима
  CreateFunction({
    name: "createNewProject",
    body: Query(
        Lambda(
          ["owner_id", "name", "description", "completed", "create_timestamp"],
          Create(Collection("Project"), {
            data: {
              name: Var("name"),
              description: Var("description"),
              completed: Var("completed"),
              create_timestamp: Var("create_timestamp"),
              owner: Ref(Collection("User"), Var("owner_id"))
            }
          })
        )
      )
  })
Войти в полноэкранный режим Выход из полноэкранного режима
  CreateFunction({
    name: "createNewUser",
    body: Query(
        Lambda(
          ["username", "password", "create_timestamp"],
          Create(Collection("User"), {
            data: {
              username: Var("username"),
              password: Var("password"),
              create_timestamp: Var("create_timestamp")
            }
          })
        )
      )
  })
Войти в полноэкранный режим Выход из полноэкранного режима
  CreateFunction({
    name: "deleteSingleProject",
    body: Query(Lambda(["id"], Delete(Ref(Collection("Project"), Var("id")))))
  })
Войти в полноэкранный режим Выход из полноэкранного режима
  CreateFunction({
    name: "updateSingleProject",
    body: Query(
        Lambda(
          ["project_id", "name", "description", "completed", "create_timestamp"],
          Update(Ref(Collection("Project"), Var("project_id")), {
            data: {
              name: Var("name"),
              description: Var("description"),
              completed: Var("completed"),
              create_timestamp: Var("create_timestamp")
            }
          })
        )
      )
  })
Войти в полноэкранный режим Выход из полноэкранного режима

Запрос данных

В этом подразделе вы обновите классы клиента GraphQL и контроллера с помощью определенного метода.

Обновление класса клиента GraphQL

Поскольку пользовательские резолверы работают, методы класса Fauna теперь могут быть обновлены. Класс Fauna инкапсулирует все запросы, которые будут выполняться в приложении, внутри своих методов.

Откройте файл app/lib/Fauna.php и обновите его, чтобы он выглядел так, как показано ниже:

<?php

namespace AppLib;

use DotenvDotenv;
use Exception;
use GraphQLClient;
use GraphQLQuery;
use GraphQLMutation;
use GraphQLRawObject;

// load env variables to $_ENV super global
$dotenv = Dotenv::createImmutable(__DIR__ . '/../../');
$dotenv->load();

define('CLIENT_SECRET', $_ENV['SECRET_KEY']);
define('FAUNA_GRAPHQL_BASE_URL', 'https://graphql.fauna.com/graphql');

class Fauna {
    public static function getClient(): Client {
        return new Client(
            FAUNA_GRAPHQL_BASE_URL,
            ['Authorization' => 'Bearer ' . CLIENT_SECRET]
        );
    }

    public static function createNewUser(string $username, string $password, int $create_timestamp): string | object {
        try {
            $mutation = (new Mutation('createUser'))
                ->setArguments(['data' => new RawObject('{username: "' . $username . '", password: "' . $password . '", create_timestamp: ' . $create_timestamp . '}')])
                ->setSelectionSet(
                    [
                        '_id',
                        '_ts',
                        'username',
                        'password',
                        'create_timestamp'
                    ]
                );
            $result = self::getClient()->runQuery($mutation);
            return $result->getData()->createUser;
        } catch(Exception $e) {
            return $e->getMessage();
        }
    }

    public static function getUserByUsername(string $username): string | object {
        try {
            $gql = (new Query('findUserByUsername'))
                ->setArguments(['username' => $username])
                ->setSelectionSet(
                    [
                        '_id',
                        '_ts',
                        'username',
                        'password',
                        'create_timestamp'
                    ]
                );
            $result = self::getClient()->runQuery($gql);
            return $result->getData()->findUserByUsername;
        } catch(Exception $e) {
            return $e->getMessage();
        }
    }

    public static function createNewProject(string $userId, string $name, string $description, bool $completed): string | object {
        try {
            $mutation = (new Mutation('createNewProject'))
                ->setArguments(['name' => $name, 'description' => $description, 'completed' => $completed, 'owner_id' => $userId])
                ->setSelectionSet(
                    [
                        '_id',
                        '_ts',
                        'name',
                        'description',
                        'completed'
                    ]
                );
            $result = self::getClient()->runQuery($mutation);
            return $result->getData()->createNewProject;
        } catch(Exception $e) {
            return $e->getMessage();
        }
    }

    public static function getProjectsByUser(string $id): string | array {
        try {
            $gql = (new Query('findProjectsByUserId'))
                ->setArguments(['owner_id' => $id])
                ->setSelectionSet(
                    [
                        '_id',
                        '_ts',
                        'name',
                        'description',
                        'completed',
                        'create_timestamp'
                    ]
                );
            $result = self::getClient()->runQuery($gql);
            return $result->getData()->findProjectsByUserId;
        } catch(Exception $e) {
            return $e->getMessage();
        }
    }

    public static function updateExistingProject(string $projectId, string $name, string $description, bool $completed): string | object {
        try {
            $mutation = (new Mutation('updateSingleProject'))
                ->setArguments(['project_id' => $projectId, 'name' => $name, 'description' => $description, 'completed' => $completed])
                ->setSelectionSet(
                    [
                        '_id',
                        '_ts',
                        'name',
                        'description',
                        'completed'
                    ]
                );
            $result = self::getClient()->runQuery($mutation);
            return $result->getData()->updateSingleProject;
        } catch(Exception $e) {
            return $e->getMessage();
        }
    }

    public static function deleteExistingProject(string $projectId): string | object {
        try {
            $mutation = (new Mutation('deleteSingleProject'))
                ->setArguments(['id' => $projectId])
                ->setSelectionSet(
                    [
                        '_id',
                        '_ts',
                        'name',
                        'description',
                        'completed'
                    ]
                );
            $result = self::getClient()->runQuery($mutation);
            return $result->getData()->deleteSingleProject;
        } catch(Exception $e) {
            return $e->getMessage();
        }
    }

    public static function getSingleProjectByUser(string $projectId): string | object | null {
        try {
            $gql = (new Query('findProjectByID'))
                ->setArguments(['id' => $projectId])
                ->setSelectionSet(
                    [
                        '_id',
                        '_ts',
                        'name',
                        'description',
                        'completed',
                        'create_timestamp'
                    ]
                );
            $result = self::getClient()->runQuery($gql);
            return $result->getData()->findProjectByID;
        } catch(Exception $e) {
            return $e->getMessage();
        }
    }
}
Вход в полноэкранный режим Выйти из полноэкранного режима

Здесь классы Query и Mutation принимают в качестве аргумента имя запроса, а метод setArguments принимает ассоциативный массив аргументов запроса и соответствующие им значения. Метод setSelectionSet определяет набор данных, которые вы хотите получить от конечной точки GraphQL. Клиент GraphQL (getClient()) предоставляет методы runQuery и getData для выполнения запроса и получения результата соответственно.

Обновление классов контроллеров

Обновите два класса контроллеров, поскольку класс Fauna уже создан.

Откройте файл app/controller/UserController.php и обновите его, как показано ниже:

<?php

namespace AppController;
use AppHelperView;
use AppLibFauna;

class UserController {
    // default time to live
    private $ttl = 30;

    // show login form
    public function login() {
        View::render('login.php', array(), 'Login - Faproman');
    }

    // show register form
    public function register() {
        View::render('register.php', array(), 'Register - Faproman');
    }

    // logout user
    public function logout() {
        // clear session
        session_unset();   
        session_destroy();
        header('Location: /login');
    }

    // create new user
    public function create() {
        $errorMsgs = array();
        if (empty($_POST['username'])) array_push($errorMsgs, 'Username is required');
        if (!preg_match('/^[A-Z0-9]*$/i', $_POST['username'])) array_push($errorMsgs, 'Username can only contain alphanumeric characters');
        if (empty($_POST['password1'])) array_push($errorMsgs, 'Password is required');
        if ($_POST['password1'] != $_POST['password2']) array_push($errorMsgs, 'Passwords must be the same');

        if (!empty($errorMsgs)) {
            $_SESSION['register_errors'] = $errorMsgs;
            return header('Location: /register');
        } 

        $newUser = Fauna::createNewUser(strtolower($_POST['username']), password_hash($_POST['password1'], PASSWORD_DEFAULT), time());

        if (gettype($newUser) == 'string') {
            preg_match('/not unique/i', $newUser) ? array_push($errorMsgs, 'Username is taken, use another') : array_push($errorMsgs, 'Something went wrong');
            $_SESSION['register_errors'] = $errorMsgs;
            return header('Location: /register');
        }

        $_SESSION['logged_in_user'] = $newUser;
        $_SESSION['ttl'] = $this->ttl;

        return header('Location: /');        
    }

    // login user
    public function authenticate() {
        $errorMsgs = array();
        if (empty($_POST['username'])) array_push($errorMsgs, 'Username is required');
        if (empty($_POST['password'])) array_push($errorMsgs, 'Password is required');
        if (!preg_match('/^[A-Z0-9]*$/i', $_POST['username'])) array_push($errorMsgs, 'Username or password is incorrect');

        if (!empty($errorMsgs)) {
            $_SESSION['login_errors'] = $errorMsgs;
            return header('Location: /login');
        } 

        $user = Fauna::getUserByUsername($_POST['username']);

        // verify that user exist
        if (gettype($user) == 'string') {
            preg_match('/not found/i', $user) ? array_push($errorMsgs, 'Username or password is incorrect') : array_push($errorMsgs, 'Something went wrong');
            $_SESSION['login_errors'] = $errorMsgs;
            return header('Location: /login');
        }

        // verify that password is correct
        if (!password_verify($_POST['password'], $user->password)) {
            array_push($errorMsgs, 'Username or password is incorrect');
            $_SESSION['login_errors'] = $errorMsgs;
            return header('Location: /login');
        }

        $_SESSION['logged_in_user'] = $user;
        $_SESSION['ttl'] = $this->ttl;

        return header('Location: /'); 
    }

}
Вход в полноэкранный режим Выход из полноэкранного режима

Откройте файл app/controller/ProjectController.php и обновите его в соответствии со следующим:

<?php

namespace AppController;
use AppHelperView;
use AppLibFauna;

class ProjectController {

    // home page
    public function index() {
        View::render('home.php', array(), 'Home Page');
    }

    // edit page
    public function edit(string $id) {
        View::render('edit.php', array('projectId' => $id), 'Edit Project');
    }

    // create new user
    public function create() {
        $errorMsgs = array();
        if (empty($_POST['name'])) array_push($errorMsgs, 'Project name is required');
        if (empty($_POST['description'])) array_push($errorMsgs, 'Description is required');

        if (!empty($errorMsgs)) {
            $_SESSION['project_errors'] = $errorMsgs;
            return header('Location: /');
        }

        $userId = $_SESSION['logged_in_user']->_id;
        $name = htmlentities($_POST['name'], ENT_QUOTES, 'UTF-8');
        $description = htmlentities($_POST['description'], ENT_QUOTES, 'UTF-8');
        $completed = isset($_POST['completed']);

        $newProject = Fauna::createNewProject($userId, $name, $description, $completed);

        if (gettype($newProject) == 'string') {
            array_push($errorMsgs, 'Something went wrong');
            $_SESSION['project_errors'] = $errorMsgs;
            return header('Location: /register');
        }

        return header('Location: /');        
    }

    // update a project
    public function update() {
        $errorMsgs = array();
        if (empty($_POST['name'])) array_push($errorMsgs, 'Project name is required');
        if (empty($_POST['description'])) array_push($errorMsgs, 'Description is required');

        $projectId = htmlentities($_POST['project_id'], ENT_QUOTES, 'UTF-8');

        if (!empty($errorMsgs)) {
            $_SESSION['project_errors'] = $errorMsgs;
            return header('Location: /edit/'.$projectId);
        }

        $name = htmlentities($_POST['name'], ENT_QUOTES, 'UTF-8');
        $description = htmlentities($_POST['description'], ENT_QUOTES, 'UTF-8');
        $completed = isset($_POST['completed']);

        $newProject = Fauna::updateExistingProject($projectId, $name, $description, $completed);

        if (gettype($newProject) == 'string') {
            array_push($errorMsgs, 'Something went wrong');
            $_SESSION['project_errors'] = $errorMsgs;
            return header('Location: /');
        }

        return header('Location: /');        
    }

    // delete a project
    public function delete() {
        $errorMsgs = array();     
        $projectId = htmlentities($_POST['project_id'], ENT_QUOTES, 'UTF-8');

        $newProject = Fauna::deleteExistingProject($projectId);

        if (gettype($newProject) == 'string') {
            array_push($errorMsgs, 'Something went wrong');
            $_SESSION['project_errors'] = $errorMsgs;
            return header('Location: /');
        }

        return header('Location: /');        
    }

}
Вход в полноэкранный режим Выход из полноэкранного режима

Аутентификация и управление сессиями

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

Запуск сессии

Функция session_start() используется для создания или возобновления сессии. Она должна вызываться в начале каждого файла, в котором вы хотите, чтобы присутствовала сессия. Вызов функции в файле index.php сделает сессию доступной везде в приложении, так как все запросы будут перенаправляться на нее.

Откройте файл public/index.php и вызовите метод session_start() в верхней части:

<?php
session_start();
/**
 * Autoload classes
 */ 
require __DIR__ . '/../vendor/autoload.php';

// if user is logged in but inactive for given TTL time then logout user
if (
    isset($_SESSION['logged_in_user']) && 
    isset($_SESSION['ttl']) && 
    isset($_SESSION['last_activity']) && 
    (time() - $_SESSION['last_activity'] > ($_SESSION['ttl'] * 60))
) {
    session_unset();
    session_destroy();
    header('Location: /login');    
} 

// record current time
$_SESSION['last_activity'] = time();
//..
//..
Вход в полноэкранный режим Выйти из полноэкранного режима

Зарегистрироваться

Откройте файл view/register.php и добавьте следующее:

<?php 
if (isset($_SESSION['register_errors'])) {
    $regErrors = $_SESSION['register_errors'];
    unset($_SESSION['register_errors']);
}
if (isset($_SESSION['logged_in_user'])) {
    header('Location: /');
}
?>

<div class="container my-5 text-center">
    <h2>Sign Up to Faproman</h1>
        <p>Sign up a new Faproman account</p>

        <form class="text-start mx-auto mt-3" method="post" action="/user/create" style="max-width: 400px;">
            <?php
            if (isset($regErrors) && !empty($regErrors)) {
            ?>
                <div class="form-error-box">
                    <?php
                    foreach ($regErrors as $value) {
                        echo $value . '<br>';
                    } 
                    ?>
                </div>
            <?php
            }
            ?>
            <div class="mb-3">
                <label for="username" class="form-label">Username</label>
                <input type="text" required class="form-control" name="username" id="username">
            </div>
            <div class="mb-3">
                <label for="password1" class="form-label">Password</label>
                <input type="password" required class="form-control" name="password1" id="password1">
            </div>
            <div class="mb-2">
                <label for="password2" class="form-label">Repeat Password</label>
                <input type="password" required class="form-control" name="password2" id="password2">
            </div>
            <div class="form-text text-end mb-4">Have an account? <a href="/login">Login</a></div>
            <button type="submit" class="btn text-white bg-dark d-block w-100">Sign Up</button>
        </form>
</div>
Вход в полноэкранный режим Выйти из полноэкранного режима

Перейдите по адресу https://faproman.test/register. Ваш экран должен выглядеть как на картинке ниже, предлагая вам создать учетную запись.

Атрибут action формы регистрации указывает на /user/create, который обрабатывается методом create класса UserController. Этот метод также обрабатывает проверку и хеширование пароля перед использованием метода Fauna::createNewUser() для записи в базу данных Fauna.

Когда пользователь успешно создан, он входит в систему, устанавливая суперглобальный элемент $_SESSION['logged_in_user'] на объект пользователя, возвращенный из запроса GraphQL. После этого пользователь перенаправляется на главную страницу.

Кроме того, через $_SESSION['ttl'] устанавливается время жизни (TTL) в тридцать минут. Если пользователь не будет взаимодействовать с системой более тридцати минут, он выйдет из нее.

Вход в систему

Откройте файл view/login.php и добавьте следующее:

<?php 
if (isset($_SESSION['login_errors'])) {
    $loginErrors = $_SESSION['login_errors'];
    unset($_SESSION['login_errors']);
}
if (isset($_SESSION['logged_in_user'])) {
    header('Location: /');
}
?>

<div class="container my-5 text-center">
    <h2>Login to Faproman</h1>
        <p>You must login to manage your projects</p>
        <form method="post" action="/user/authenticate" class="text-start mx-auto mt-3" style="max-width: 400px;">
            <?php
            if (isset($loginErrors) && !empty($loginErrors)) {
            ?>
                <div class="form-error-box">
                    <?php
                    foreach ($loginErrors as $value) {
                        echo $value . '<br>';
                    } 
                    ?>
                </div>
            <?php
            }
            ?>
            <div class="mb-3">
                <label for="username" class="form-label">Username</label>
                <input type="text" required class="form-control" name="username" id="username">
            </div>
            <div class="mb-2">
                <label for="password" class="form-label">Password</label>
                <input type="password" required class="form-control" name="password" id="password">
            </div>
            <div class="form-text text-end mb-4">New? <a href="/register">Sign Up</a></div>
            <button type="submit" class="btn text-white bg-dark d-block w-100">Login</button>
        </form>
</div>
Войти в полноэкранный режим Выйти из полноэкранного режима

Перейдите по адресу https://faproman.test/login. Теперь ваш экран должен выглядеть как показано на рисунке ниже, предлагая пользователям войти в систему, чтобы получить доступ к своим проектам.

Атрибут action формы входа указывает на /user/authenticate, который обрабатывается методом authenticate класса UserController. Метод также извлекает пользователя из Fauna с помощью метода Fauna::getUserByUsername().

Если пользователь найден и пароль проверен, он входит в систему, устанавливая суперглобальный элемент $_SESSION['logged_in_user'] на объект user, возвращенный из запроса GraphQL. Затем пользователь перенаправляется на главную страницу.

Как и раньше, время жизни (TTL) в тридцать минут устанавливается через $_SESSION['ttl']. Если пользователь не будет взаимодействовать с системой более тридцати минут, он выйдет из нее.

Формы и обновления

В этом подразделе вы настроите формы, которые позволят пользователям создавать, редактировать и обновлять свои проекты.

Главная страница

Главная страница состоит из формы для добавления новых проектов, формы для удаления проектов и меню-аккордеона для отображения существующих проектов. Проекты пользователя можно получить с помощью метода Fauna::getProjectsByUser().

Откройте файл view/home.php и добавьте следующее:

<?php
use AppLibFauna;

if (!isset($_SESSION['logged_in_user'])) {
    header('Location: /login');
}
$loggedInUser = $_SESSION['logged_in_user'];

if (isset($_SESSION['project_errors'])) {
    $projectErrors = $_SESSION['project_errors'];
    unset($_SESSION['project_errors']);
}

$userProjects = Fauna::getProjectsByUser($loggedInUser->_id);
?>

<div class="container my-3">
    <h2 class="mt-5">Welcome @<?php echo $loggedInUser->username ?></h2>
    <p>Manage your projects here.</p>
    <form method="post" action="/project/create" class="text-start mt-3">
        <?php
        if (isset($projectErrors) && !empty($projectErrors)) {
        ?>
        <div class="form-error-box">
            <?php
                foreach ($projectErrors as $value) {
                    echo $value . '<br>';
                } 
                ?>
        </div>
        <?php
        }
        ?>
        <div class="form-floating mb-3">
            <input type="text" required class="form-control" id="floatingInput" name="name"
                placeholder="Name of project">
            <label for="name">Project Name</label>
        </div>
        <div class="form-floating">
            <textarea required class="form-control" placeholder="Enter project description here..." name="description"
                id="description" style="height: 100px"></textarea>
            <label for="description">Description</label>
        </div>
        <div class="form-check mt-2">
            <input class="form-check-input" type="checkbox" name="completed" id="completed">
            <label class="form-check-label" for="completed">
                Completed
            </label>
        </div>
        <button type="submit" class="btn text-white bg-dark mt-3">Add Project</button>
    </form>

    <h4 class="mt-5">All Your Projects</h4>
    <p><?php echo "No of projects: " . count($userProjects); ?></p>
    <div class="accordion my-3 mb-5" id="accordionExample">
        <?php 
        if (gettype($userProjects) == "array"):
            for ($i = 0; $i < count($userProjects); $i++): 
            $project = $userProjects[$i];
        ?>
        <div class="accordion-item">
            <h2 class="accordion-header"  id="headingOne<?php echo $i; ?>">
                <button class="accordion-button <?php echo $i != 0 ? "collapsed" : null; ?>" type="button" data-bs-toggle="collapse"
                    data-bs-target="#collapse<?php echo $i; ?>" aria-expanded="true"
                    aria-controls="collapse<?php echo $i; ?>">
                    <?php echo $project->name; ?>
                </button>
            </h2>
            <div id="collapse<?php echo $i; ?>"
                class="accordion-collapse collapse <?php echo $i == 0 ? "show" : null; ?>"
                aria-labelledby="headingOne<?php echo $i; ?>"
                data-bs-parent="#accordionExample">
                <div class="accordion-body">
                    <?php echo $project->description; ?>
                </div>
                <div class="border m-2 p-2 d-flex justify-content-between align-items-center">
                    <div>Completed: <strong><?php echo $project->completed ? "Yes" : "No" ?></strong></div>
                    <div class="d-flex">
                        <a href="/edit/<?php echo $project->_id ?>" class="btn btn-outline-dark border-0 py-0 px-1 me-1"><i class="bi bi-pencil-square"></i></a>
                        <form action="/project/delete" method="post">
                            <input type="hidden" name="project_id" value="<?php echo $project->_id ?>">
                            <button type="submit" class="btn btn-outline-danger border-0 py-0 px-1"><i class="bi bi-trash"></i></button>
                        </form>
                    </div>
                </div>
            </div>
        </div>
        <?php
            endfor;
        endif;
        ?>
    </div>
</div>
Войти в полноэкранный режим Выйти из полноэкранного режима

Перейдите по адресу https://faproman.test. Войдите в систему, если вы не вошли, и создайте новый проект через форму. Ваш экран должен выглядеть так, как показано ниже.

Атрибут action формы создания проекта указывает на /project/create, который обрабатывается методом create класса ProjectController. Метод также обрабатывает проверку перед использованием метода Fauna::createNewProject() для записи в базу данных Fauna.

Атрибут action формы удаления проекта указывает на /project/delete, который обрабатывается методом delete класса ProjectController, который использует Fauna::deleteExistingProject() для удаления проекта.

Обновление проекта

Страница редактирования состоит из формы для обновления проектов.

Откройте файл view/edit.php и добавьте следующее:

<?php
use AppLibFauna;

if (!isset($_SESSION['logged_in_user'])) {
    header('Location: /login');
}
$loggedInUser = $_SESSION['logged_in_user'];

if (isset($_SESSION['project_errors'])) {
    $projectErrors = $_SESSION['project_errors'];
    unset($_SESSION['project_errors']);
}

$project = Fauna::getSingleProjectByUser($projectId);
?>

<div class="container my-3">
    <?php if (gettype($project) != 'object'): ?>
    <h2 class="mt-5">Project Not Found</h2>
    <?php else: ?>
    <h2 class="mt-5">Update Project</h2>
    <p>Update your project here.</p>
    <form method="post" action="/project/update" class="text-start mt-3">
        <?php
        if (isset($projectErrors) && !empty($projectErrors)) {
        ?>
        <div class="form-error-box">
            <?php
                foreach ($projectErrors as $value) {
                    echo $value . '<br>';
                } 
                ?>
        </div>
        <?php
        }
        ?>
        <div class="form-floating mb-3">
            <input type="text" required class="form-control" id="floatingInput" name="name"
                placeholder="Name of project" value="<?php echo $project->name ?>">
            <label for="name">Project Name</label>
        </div>
        <div class="form-floating">
            <textarea required class="form-control" placeholder="Enter project description here..." name="description"
                id="description" style="height: 100px"><?php echo $project->description ?></textarea>
            <label for="description">Description</label>
        </div>
        <div class="form-check mt-2">
            <input class="form-check-input" type="checkbox" name="completed" id="completed" <?php echo $project?->completed ? "checked" : null ?>>
            <label class="form-check-label" for="completed">
                Completed
            </label>
        </div>
        <input type="hidden"  name="project_id" value="<?php echo $project->_id ?>">
        <button type="submit" class="btn text-white bg-dark mt-3">Edit Project</button>
        <a href="/" class="btn btn-outline-dark mt-3 ms-2">Cancel</a>
    </form>
    <?php endif; ?>
</div>
Войти в полноэкранный режим Выйти из полноэкранного режима

Снова перейдите по адресу https://faproman.test. Войдите в систему, если вы еще не вошли, и создайте новый проект через форму, если он еще не существует. Нажмите на кнопку редактирования в аккордеоне. Теперь ваш экран должен выглядеть так, как показано ниже:

Атрибут action формы создания проекта указывает на /project/update, который обрабатывается методом update класса ProjectController. Метод также обрабатывает проверку перед использованием метода Fauna::updateExistingProject() для обновления документа в базе данных Fauna.

Заключение

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

Fauna — это бессерверная, гибкая, удобная для разработчиков, транзакционная база данных и API данных для современных приложений. Fauna предлагает встроенную поддержку GraphQL с транзакциями, пользовательской логикой и контролем доступа. Пользователи Fauna могут легко создавать сервисы GraphQL для получения данных из своих баз данных.

Оцените статью
devanswers.ru
Добавить комментарий