How to use Fauna with GraphQL and PHP
Tags: graphql, php, fauna, database
Published: 2022-07-21 01:44:01

[Fauna](https://fauna.com/) is a flexible, distributed document-relational database delivered as a secure and scalable cloud API with built-in support for [GraphQL](https://graphql.org/). Fauna has a multimodel database paradigm, in that it has the flexibility of a NoSQL database with the relational querying and transactional capabilities of a SQL database. Fauna was built with modern applications in mind, and is a good fit for cloud-native applications that need a reliable, [ACID-compliant](https://fauna.com/blog/what-is-acid-compliance-atomicity-consistency-isolation), cloud-based service to support audiences in multiple regions.  

Fauna provides unique features such as:

- Ability to fetch nested data in a single call
- Optimized bandwidth usage 
- Minimal administrative overhead 
- Low transactional latency transactional with a fast read/write operation—no matter where your client is running
- Powerful NoSQL queries, including the ability to make joins and program business logic using a very expressive [native query language](https://docs.fauna.com/fauna/current/api/fql/) 
- Advanced indexing 
- [Multitenancy](https://docs.fauna.com/fauna/current/learn/tutorials/fql/multitenant), 
- Database implementation abstraction layer via GraphQL

These features make Fauna well suited for handling large amounts of structured and unstructured data.

This guided tutorial shows you how you can use Fauna and PHP to build powerful applications. 

You will build a simple project management application that uses Fauna's GraphQL API. In this article, you'll learn more about how Fauna works with GraphQL and PHP. You'll be taken through the process of building a simple project management application that uses GraphQL and Fauna for its CRUD operations. This is what your finished application will look like:

![A GIF showing the end result of project to be built](https://imgur.com/dllj78o.gif)

If you'd like to see all the source code at once, it can be found [here](https://github.com/iamgideonidoko/faproman).

## Building with Fauna, GraphQL, and PHP

In this section, you'll learn how a simple project manager called Faproman is built using Fauna, GraphQL, and PHP. Vanilla PHP is used here to keep things simple. The source code for the application can be found [here](https://github.com/iamgideonidoko/faproman).

## Prerequisites

- A Fauna account, which can be created [here](https://dashboard.fauna.com/accounts/register).
- Basic knowledge of [GraphQL](https://graphql.org/learn/) and object-oriented [PHP](https://www.w3schools.com/php/).
- PHP and Composer installed. [XAMPP](https://www.apachefriends.org/) is an open source Apache distribution containing PHP. [Composer](https://getcomposer.org) is a dependency manager for PHP.

>💡Check out the [Fauna workshop for GraphQL developers](https://graphql.workshops.fauna.com/), a self-paced exploration of Fauna’s GraphQL features.

## Creating a Fauna database

Fauna offers the ability to import GraphQL schemas. When a schema is imported, Fauna:

- Creates collections for the data types in the schema, excluding the embedded types. 
- Adds a document id (`_id`) and timestamp (`ts`) fields to every document that will exist in the collection. Documents of the same type belong to the same collection. 
- Creates indexes for every named query, with search parameters derived from the field parameters for each relation between types.
- Creates helper queries and mutations to help with CRUD operations on the created collections.
- Provides a GraphQL endpoint and playground with **Schema** and **Docs** tabs to view the list of possible queries and types available.

You'll begin by creating a Fauna database. Log in to your [Fauna Dashboard](https://dashboard.fauna.com). The dashboard for a newly registered user looks like this:

![View of Fauna's dashboard for newly registered users](https://i.imgur.com/BOCuu3C.png)

Click on **Create Database**. 

On the Create Database form, fill in the **Name** field with the database name (such as Faproman) and select your region group. The **Region Group** field lets you decide the geographic region where your data will reside. If you're not sure what region to choose, select **Classic (C)**.

![New database form](https://i.imgur.com/TNWV6IE.png)

Leave the **Use demo data** field unchecked, as no demo data is needed for this project.

Click **CREATE**, and your new database will be created. 

## Uploading a GraphQL schema

Before any GraphQL API interaction with our database can occur, Fauna needs a GraphQL schema. First, let's define the structure of the schema that suits the application.

The project management application to be built has a [one-to-many relationship](https://docs.fauna.com/fauna/current/api/graphql/relationships#one2many), because one user can have many projects. This kind of relationship can be represented in a GraphQL schema by two types: the source (for user) and the target (for projects). The source type will have an array field pointing to the target type, while the target will have a non-array field pointing back to the source.

The source (`User`) and target (`Project`) types are given below:

```gql
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
}
```

The `@relation` directive on the `project` field of the `User` type helps to ensure that Fauna recognizes the field as one that creates a relationship, rather than an array of IDs. The `@unique` directive on the `username` field of the `User` types ensures that no two users can have the same username.

The `Query` type is defined below:

```gql
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
}
```

The `@resolver` directive on the query fields defines the user-defined function to be used to resolve the queries. The `name` field specifies the name of the function.

The `Mutation` type is defined below:

```gql
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")

}
```

The `@resolver` directive on the mutation fields also defines the user-defined function to be used to resolve the mutations. These mutations will help create, update, and delete new projects and create new users.

Create a file named `schema.gql` and paste the source, target, query, and mutation types inside it. The schema is now ready to be uploaded. 

To upload, click on **GraphQL** from the sidebar on your Fauna dashboard. You'll see an interface that looks like this:

![New user GraphQL section](https://imgur.com/IcZLeOk.png)

Click on **IMPORT SCHEMA**, then select the `schema.gql` file you created earlier. After the upload completes successfully, you'll see the GraphQL playground, which you can use to interact with the GraphQL API of your Fauna database.

Note that aside from the queries and mutation specified in the schema, Fauna automatically creates basic CRUD queries and mutations for both the source (`User`) and target (`Project`) types. These auto generated queries and mutations are automatically resolved by Fauna, meaning you can start using them right off the bat. Custom resolvers have to be created to use the queries and mutations specified in the schema file—you'll create them later in the tutorial. 

## Client setup

In this subsection, you'll set up the project management application on your local machine.

### Project structure

The file structure of Faproman is given below:

```text
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   
```

### Create project directory

Locate the `htdocs` folder, or the directory that your Apache web server looks for files to serve by default, and create a folder in that directory called `faproman`. For XAMPP, the directory is found in `/xampp/htdocs`. 

Create a `public` subfolder inside `faproman` directory. Your Apache server will be reconfigured later on to serve files from the public folder specifically for Faproman.

### Set up a virtual host locally

A virtual host is an Apache server configuration rule that allows for the specification of a site's document root (the directory or folder containing the site's files). A virtual host will help you set up a local domain, like `faproman.test`, that points to the project's public folder any time the domain is accessed.

To set up a virtual host on your local machine, begin by updating the hosts file of your operating system. The hosts file maps host names to IP addresses.

- For Windows OS, locate the hosts file at `C:/Windows/System32/drivers/etc/hosts`
- For Mac OS, locate the hosts file at ` /etc/hosts`

Open the file in your preferred editor and add the below at the bottom:

```text
127.0.0.1	localhost
127.0.0.1	faproman.test # for Faproman app
```

Update the virtual hosts file. 

#### On Windows

On Windows OS (xampp), the file is located at `c:/xampp/apache/conf/extra/httpd-vhosts.conf`.

Open the file in your preferred editor and add the below at the bottom:

```text
<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>
```

#### On Mac

On Mac OS (xampp), the file is located at `/Applications/XAMPP/xamppfiles/etc/extra/httpd-vhosts.conf`.

Open the file in your preferred editor and add the below at the bottom:

```text
<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>
```

#### For both operating systems

Next, create an `index.php` file inside of `/htdocs/faproman/public`, and add the below code inside it to output the text “This is a test”:

  ```php
   <?php
   echo "This is a test";
   ```
.
Start your Apache server via xampp's control panel.

Navigate to `http://faproman.test` or `http://faproman.test:8080` in your browser. You’ll see the text you just output.

![Homepage](https://i.imgur.com/kPwjfkI.png)

### Install required dependencies

Create a `composer.json ` file in the root directory of Faproman (`/htdocs/faproman`) and add the following code:

```json
{
    "autoload": {
        "psr-4": {
            "App\\": "app/"
        }
    }
}
```

This ensures that any namespace starting with `App\` is mapped to the `app/` folder in the application.

Open your terminal in the `faproman` directory, and run the command below to install all the required dependencies:

```bash
composer require altorouter/altorouter gmostafa/php-graphql-client vlucas/phpdotenv
```

This installs the following packages:

- **altorouter/altorouter:** For managing routes.
- **gmostafa/php-graphql-client:** For making requests to a GraphQL endpoint.
- **vlucas/phpdotenv**: For loading environment variables from `.env` files.

### Create the required folders and files

Create all the folders and files inside the `faproman` folder to match the project structure above. Open the `faproman` folder in your favorite code editor.

### Handle routing

The `.htaccess` config file can be used to make Apache servers redirect requests to all PHP files in the application to a single PHP file.

Open the `/public/.htaccess` file and paste in the below configuration:

```text
RewriteEngine on
RewriteBase /
RewriteCond %{REQUEST_FILENAME} !-f
RewriteRule . index.php [L]
```

This will redirect requests to the `index.php` file in the `public` directory, allowing you to better handle routing.

Open the `public/index.php` file and paste in the code below to set up routing using the `altorouter/altorouter` package:

```php
<?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"
}
```

### Handle views

Open the `app/helper/View.php` file and enter the following code to create a helper class that will render the view files in the `view` directory.

```php
<?php

namespace App\Helper;

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');
    }

}

```

### Create controller classes

A *controller class* is used to group request handling logic in methods. There are two controllers in the application.

- **UserController:** handles all requests related to users.
- **ProjectController:** handles all requests related to the project.

Open the `app/controller/UserController.php` file and create the UserController class as shown below:

```php
<?php

namespace App\Controller;
use \App\Helper\View;

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() {}

}
```

Open the `app/controller/ProjectController.php` file and create the ProjectController class:

```php
<?php

namespace App\Controller;
use \App\Helper\View;

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() {}

}
```

Notice how the `View::render()` method is used to render the view files.

### Handle routes with controller classes

Update the `public/index.php` file as follows:

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

use \App\Controller\ProjectController;
use \App\Controller\UserController;
use \App\Helper\View;

$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();
}
```

Now routing will be properly handled. Whenever a request matches any of the routes specified, the corresponding controller method is called.

### Update template and CSS files

Open each of the following files, and add the relevant code to each of them.

To define the header, edit  `temp/header.php`:

  ```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>
  ```

Bootstrap's JavaScript file is added just before the closing `</body>` to make bootstrap components that require JavaScript to work properly.

To define the footer, edit `temp/footer.php`:

  ```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>
  ```

Bootstrap's JavaScript file is added just before the closing `</body>` to make bootstrap components that require JavaScript to work properly.

To define the navbar, edit `temp/nav.php`:

  ```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>
  ```

To define the 404 page, edit `temp/404.php`:

  ```php
  <div class="page-404">
      404 | Not Found
  </div>
  ```

To import the font and icons, and add additional styling, edit `public/assets/style.css`:

  ```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;
  }
  ```

### Configure your GraphQL client

Now you'll create a `Fauna` class that will handle all interactions with your database. Open the `app/lib/Fauna.php` and paste the below:

```php
<?php

namespace App\Lib;

use Dotenv\Dotenv;
use GraphQL\Client;

// 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) {}
```

The `$dotenv->load()` method loads all environment variables from the `.env` file into `$_ENV` and `$_SERVER` superglobals. The keys from your Fauna database should be stored in a `.env` file in the root directory of `faproman`, which will make the keys available in the aforementioned superglobals.

The `Fauna::getClient()` method returns a new configured instance of your GraphQL client.

### Creating a Fauna database access key

Fauna uses secrets like access keys to authenticate clients. Follow the below steps to create one.

Navigate to your database dashboard and click on **[ Security ]** from the sidebar.

![New access key](https://imgur.com/Q2Lrv0n.png)

Click on **New key**.

![New key form](https://imgur.com/XQrsTxO.png)

In the key creation form, select the current database as your database, Server as the role, and `faproman_app` as the key name. 

Note that a new database comes with two roles by default: Admin and Server. You can also create custom roles with your desired privileges. The Server option is selected because all interaction with the database will be done via PHP—a server-side language.

Click on **Save** to save your key. Copy the key's secret and save it somewhere safe. Additionally, paste the key's secret in your `.env` file in the root directory with the variable name `SECRET_KEY`.

Now your application is ready to make calls to Fauna.

## Custom resolvers and user-defined functions (UDF)

Recall that in the schema for Faproman discussed earlier, the `@resolver` directive was used to specify user-defined functions that will be used as custom resolvers to resolve the queries or mutations. User-defined functions that `@resolver` directives specify have to be created for custom resolvers to work when the fields are queried.

User-defined functions can be created either through Fauna's built-in shell, or through the function creation form. They are defined by the functions provided by [Fauna Query Language (FQL)](https://docs.fauna.com/fauna/v4/api/fql/cheat_sheet).

Before proceeding, run the following in your shell to create `project_ower_idx` and `user_username_idx` indexes.

```shell
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
})
```

![Shell](https://i.imgur.com/tiqmmFt.png)

Run the following commands to create the user-defined functions (custom resolvers) as defined in the schema of the application. 

**NB:** If any of the functions below already exist, click on **[ Function ]** on your Fauna dashboard, click on that function and click on **[ Delete ]** below the function's definition to delete the function. Run the command in the shell to recreate the function.

- `findUserByUsername`

  ```shell
  CreateFunction({
    name: "findUserByUsername",
    body: Query(
        Lambda(
          ["username"],
          Get(
            Select(
              ["data", 0],
              Paginate(Match(Index("user_username_idx"), Var("username")))
            )
          )
        )
      )
  })
  ```

- `findProjectsByUserId`

  ```shell
  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")))
            )
          )
        )
      )
  })
  ```

- `createNewProject`

  ```shell
  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"))
            }
          })
        )
      )
  })
  ```

- `createNewUser`

  ```shell
  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")
            }
          })
        )
      )
  })
  ```

- `deleteSingleProject`

  ```shell
  CreateFunction({
    name: "deleteSingleProject",
    body: Query(Lambda(["id"], Delete(Ref(Collection("Project"), Var("id")))))
  })
  ```

- `updateSingleProject`

  ```shell
  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")
            }
          })
        )
      )
  })
  ```

## Querying data

In this subsection, you'll update your GraphQL client and controller classes with a defined method.

### Update GraphQL client class

Since the custom resolvers are up, the methods of the `Fauna` class can now be updated. The `Fauna` class encapsulates all the queries that'll be run on the app, inside its methods.

Open the `app/lib/Fauna.php` file and update it to look like the below:

```php
<?php

namespace App\Lib;

use Dotenv\Dotenv;
use Exception;
use GraphQL\Client;
use GraphQL\Query;
use GraphQL\Mutation;
use GraphQL\RawObject;

// 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();
        }
    }
}
```

Here, the `Query` and `Mutation` classes take in the name of the query as an argument, and the `setArguments` method takes in an associative array of query arguments and their respective values. The `setSelectionSet` method specifies the set of data you want from the GraphQL endpoint. The GraphQL client (`getClient()`) provides the `runQuery` and `getData` methods to run a query and get the result, respectively.

### Update controller classes

Update the two controller classes, since the `Fauna` class is now all set.

Open the `app/controller/UserController.php` file and update it as shown below:

```php
<?php

namespace App\Controller;
use \App\Helper\View;
use \App\Lib\Fauna;

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: /'); 
    }

}
```

Open the `app/controller/ProjectController.php` file and update it to match the following:

```php
<?php

namespace App\Controller;
use \App\Helper\View;
use \App\Lib\Fauna;

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: /');        
    }

}
```
## Authentication and session management

In this subsection, you'll set up user authentication for the application. You'll also manage sessions using PHP's built-in session feature.

### Start session

The `session_start()` function is used to create or resume a session. It must be called at the top of every file in which you want a session to be present.  Calling the function in the `index.php` file will make a session available everywhere in the application, since all requests are redirected to it.

Open the `public/index.php` file and call the `session_start()` method at the top:

```php
<?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();
//..
//..
```

### Sign up

Open the `view/register.php` file and add the following:

```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>
```

Navigate to `https://faproman.test/register`. Your screen should look like the image below, prompting you to create an account. 

![Register page](https://imgur.com/V4FX0Ls.png)

The `action` attribute of the sign-up form points to `/user/create`, which is handled by the `create` method of the `UserController` class. This method also handles validation and password hashing before using the `Fauna::createNewUser()` method to write to the Fauna database. 

When the user is successfully created, the user is logged into the system by setting the `$_SESSION['logged_in_user']` superglobal item to the user object returned from the GraphQL query. After that, the user is redirected to the home page. 

Additionally, a time to live (TTL) of thirty minutes is set via `$_SESSION['ttl']`. This will log the user out of the system if there's no interaction for more than thirty minutes.

### Log in

Open the file `view/login.php` and add the following:

```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>
```

Navigate to `https://faproman.test/login`. Your screen should now look like the image below, prompting users to log in to access their projects. 

![Login page](https://imgur.com/zIvZzu1.png)

The `action` attribute of the login form points to `/user/authenticate`, which is handled by the `authenticate` method of the `UserController` class. The method also fetches the user from Fauna using the `Fauna::getUserByUsername()` method. 

If the user is found and the password is verified, the user is logged into the system by setting the `$_SESSION['logged_in_user']` superglobal item to the user object returned from the GraphQL query. The user is then redirected to the home page. 

As before, a time to live (TTL) of thirty minutes is set via `$_SESSION['ttl']`. This will log the user out of the system if there's no interaction for more than thirty minutes.

## Forms and updates

In this subsection, you'll set up the forms that will enable users to create, edit, and update their projects.

### Home page

The home page consists of a form to add new projects, a form to delete projects, and an accordion menu to display existing projects. Projects by a user are obtained using the `Fauna::getProjectsByUser()` method.

Open the `view/home.php` file and add the below:

```php
<?php
use \App\Lib\Fauna;

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>
```

Navigate to `https://faproman.test`. Log in if you're not logged in, and create a new project via the form. Your screen should look like the one below.

![Home page](https://imgur.com/NIkiVur.png)

The `action` attribute of the project creation form points to `/project/create`, which is handled by the `create` method of the `ProjectController` class. The method also handles validation before using the `Fauna::createNewProject()` method to write to the Fauna database. 

The `action` attribute of the project deletion form points to `/project/delete`, which is handled by the `delete` method of the `ProjectController` class, which uses the `Fauna::deleteExistingProject()` to delete a project.

### Update project

The edit page consists of a form to update projects.

Open the file `view/edit.php` and add the following:

```php
<?php
use \App\Lib\Fauna;

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>
```

Navigate once again to `https://faproman.test`. Log in if you're not logged in, and create a new project via the form if one doesn't yet exist. Click on the edit button in the accordion. Your screen should now look like the one below:

![Edit page](https://imgur.com/G4JIX0l.png)

The `action` attribute of the project creation form points to `/project/update`, which is handled by the `update` method of the `ProjectController` class. The method also handles validation before using the `Fauna::updateExistingProject()` method to update the document in the Fauna database. 

## Conclusion

In this article, you learned how GraphQL works with Fauna, how to create and upload a GraphQL schema, what happens when a schema is uploaded to Fauna, and the relationship between schema types. You were taken through the process of setting up a virtual host on your local machine, configuring a GraphQL client, and creating a secret key for it. You also learned about custom resolvers and the roles they play in Fauna, and followed detailed steps on how to build a PHP application from start to finish using GraphQL and Fauna.

[Fauna](https://fauna.com/) is a serverless, flexible, developer-friendly, transactional database and data API for modern applications. Fauna offers native GraphQL support with transactions, custom logic, and access control. Fauna users can effortlessly build GraphQL services to fetch data from its databases.