Showing posts with label PHP. Show all posts
Showing posts with label PHP. Show all posts

Wednesday, February 5, 2025

Develop simple REST API with PHP and MySQL

 Overview

In this article we will develop is simple REST API with PHP. The database backend is MySQL running in a Docker container and the sample data represents students.

Source code: https://github.com/medhatelmasry/school-api

Getting Started

Start MySQL in a Docker container with:

docker run -d -p 3333:3306 --name maria -e MYSQL_ROOT_PASSWORD=secret mariadb:10.7.3

In a working directory named school-api, create the following sub-folders:

mkdir src
cd src
mkdir Controller
mkdir System
mkdir TableGateways
cd ..
mkdir public

Add a composer.json file in the top directory with just one dependency: the DotEnv library which will allow us to keep our authentication details in a .env file outside our code repository:


Contents of the composer.json file:

{
    "require": {
        "vlucas/phpdotenv": "^2.4"
    },
    "autoload": {
        "psr-4": {
            "Src\\": "src/"
        }
    }
}

We also configured a PSR-4 autoloader which will automatically look for PHP classes in the /src directory.

We can install our dependencies with:

composer install

We now have a /vendor directory, and the DotEnv dependency is installed (we can also use our autoloader to load our classes from /src with no include() calls).

Let’s create a .gitignore file for our project with two lines in it, so the /vendor directory and our local .env file are ignored if our code is pushed to source control. Add this to .gitignore:

vendor/
.env

Next, add a .env file where we’ll put our database connection details:

DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3333
DB_DATABASE=apidb
DB_USERNAME=root
DB_PASSWORD=secret

Add a bootstrap.php file to load environment variables.

<?php
error_reporting(E_ALL & ~E_DEPRECATED);

require 'vendor/autoload.php';

use Dotenv\Dotenv;

use Src\System\DatabaseConnector;

$dotenv = new DotEnv(__DIR__);
$dotenv->load();

$dbConnection = (new DatabaseConnector())->getConnection();

Configure DB for PHP REST API

We can now create a class to hold our database connection and add the initialization of the connection to our bootstrap.php file. In the src/System/ folder, add a file named DatabaseConnector.php with this code:

<?php
namespace Src\System;
class DatabaseConnector {
    private $dbConnection = null;
    public function __construct() {
        $host = getenv('DB_HOST');
        $port = getenv('DB_PORT');
        $db   = getenv('DB_DATABASE');
        $user = getenv('DB_USERNAME');
        $pass = getenv('DB_PASSWORD');
        try {
            $pdo = new \PDO("mysql:host=$host;port=$port;charset=utf8mb4", $user, $pass);
            $pdo->exec("CREATE DATABASE IF NOT EXISTS `$db`");
        } catch (\PDOException $e) {
            die("DB ERROR: " . $e->getMessage());
        }
        try {
            $this->dbConnection = new \PDO(
                "mysql:host=$host;port=$port;charset=utf8mb4;dbname=$db",
                $user,
                $pass
            );
        } catch (\PDOException $e) {
            exit($e->getMessage());
        }
    }
         public function getConnection() {
        return $this->dbConnection;
    }
}

Let’s create a dbseed.php file which creates a students table and inserts some records for testing. Code for dbseed.php is shown below:

<?php
require 'bootstrap.php';
$statement = "
    CREATE TABLE IF NOT EXISTS students (
        id INT NOT NULL AUTO_INCREMENT,
        firstname VARCHAR(40) NOT NULL,
        lastname VARCHAR(40) NOT NULL,
        school VARCHAR(40) NOT NULL,
        PRIMARY KEY (id)
    );
    INSERT INTO students
        (firstname, lastname, school)
    VALUES
        ('Mark', 'Fisher','Computing'),
        ('Lisa', 'Fisher','Computing'),
        ('Judy', 'Fisher','Computing'),
        ('Jane', 'Smith','Nursing'),
        ('Mary', 'Smith','Nursing'),
        ('Andy', 'Smith','Nursing'),
        ('Bill', 'Smith','Business'),
        ('Fred', 'Plumber','Business'),
        ('Anna', 'Plumber','Business');
";
try {
    $createTable = $dbConnection->exec($statement);
    echo "Success!\n";
} catch (\PDOException $e) {
    exit($e->getMessage());
}

Run the following command to seed the database with sample data:

php dbseed.php

Add a Gateway Class for students table

In the src/TableGateways/StudentsGateway.php class file, we will implement methods to return all students, return a specific student and add/update/delete a student:

<?php
namespace Src\TableGateways;
class StudentsGateway {
    private $db = null;
    public function __construct($db) {
        $this->db = $db;
    }
    public function findAll() {
        $statement = "
            SELECT 
                id, firstname, lastname, school
            FROM
                students;
        ";
        try {
            $statement = $this->db->query($statement);
            $result = $statement->fetchAll(\PDO::FETCH_ASSOC);
            return $result;
        } catch (\PDOException $e) {
            exit($e->getMessage());
        }
    }
    public function find($id) {
        $statement = "
            SELECT 
                id, firstname, lastname, school
            FROM
                students
            WHERE id = ?;
        ";
        try {
            $statement = $this->db->prepare($statement);
            $statement->execute(array($id));
            $result = $statement->fetchAll(\PDO::FETCH_ASSOC);
            return $result;
        } catch (\PDOException $e) {
            exit($e->getMessage());
        }    
    }
    public function insert(Array $input) {
        $statement = "
            INSERT INTO students 
                (firstname, lastname, school)
            VALUES
                (:firstname, :lastname, :school);
        ";
        try {
            $statement = $this->db->prepare($statement);
            $statement->execute(array(
                'firstname' => $input['firstname'],
                'lastname'  => $input['lastname'],
                'school' => $input['school'] ?? null,
            ));
            return $statement->rowCount();
        } catch (\PDOException $e) {
            exit($e->getMessage());
        }    
    }
    public function update($id, Array $input) {
        $statement = "
            UPDATE students
            SET 
                firstname = :firstname,
                lastname  = :lastname,
                school = :school
            WHERE id = :id;
        ";
        try {
            $statement = $this->db->prepare($statement);
            $statement->execute(array(
                'id' => (int) $id,
                'firstname' => $input['firstname'],
                'lastname'  => $input['lastname'],
                'school' => $input['school'] ?? null,
            ));
            return $statement->rowCount();
        } catch (\PDOException $e) {
            exit($e->getMessage());
        }    
    }
    public function delete($id) {
        $statement = "
            DELETE FROM students
            WHERE id = :id;
        ";
        try {
            $statement = $this->db->prepare($statement);
            $statement->execute(array('id' => $id));
            return $statement->rowCount();
        } catch (\PDOException $e) {
            exit($e->getMessage());
        }    
    }
}

Implement the PHP REST API

Our REST API will have the following endpoints:

return all recordsGET /students
return a specific recordGET /students/{id}
create a new recordPOST /students
update an existing recordPUT /students/{id}
delete an existing recordDELETE /students/{id}

Create a src/Controller/StudentsController.php to handle the API endpoints with this code:

<?php
namespace Src\Controller;
use Src\TableGateways\StudentsGateway;
class StudentsController {
    private $db;
    private $requestMethod;
    private $id;
    private $studentsGateway;
    public function __construct($db, $requestMethod, $id) {
        $this->db = $db;
        $this->requestMethod = $requestMethod;
        $this->id = $id;
        $this->studentsGateway = new StudentsGateway($db);
    }
    public function processRequest() {
        switch ($this->requestMethod) {
            case 'GET':
                if ($this->id) {
                    $response = $this->getById($this->id);
                } else {
                    $response = $this->getAll();
                };
                break;
            case 'POST':
                $response = $this->createRequest();
                break;
            case 'PUT':
                $response = $this->updateFromRequest($this->id);
                break;
            case 'DELETE':
                $response = $this->deleteById($this->id);
                break;
            default:
                $response = $this->notFoundResponse();
                break;
        }
        header($response['status_code_header']);
        if ($response['body']) {
            echo $response['body'];
        }
    }
    private function getAll() {
        $result = $this->studentsGateway->findAll();
        $response['status_code_header'] = 'HTTP/1.1 200 OK';
        $response['body'] = json_encode($result);
        return $response;
    }
    private function getById($id) {
        $result = $this->studentsGateway->find($id);
        if (! $result) {
            return $this->notFoundResponse();
        }
        $response['status_code_header'] = 'HTTP/1.1 200 OK';
        $response['body'] = json_encode($result);
        return $response;
    }
    private function createRequest() {
        $input = (array) json_decode(file_get_contents('php://input'), TRUE);
        if (! $this->validate($input)) {
            return $this->unprocessableEntityResponse();
        }
        $this->studentsGateway->insert($input);
        $response['status_code_header'] = 'HTTP/1.1 201 Created';
        $response['body'] = null;
        return $response;
    }
    private function updateFromRequest($id) {
        $result = $this->studentsGateway->find($id);
        if (! $result) {
            return $this->notFoundResponse();
        }
        $input = (array) json_decode(file_get_contents('php://input'), TRUE);
        if (! $this->validate($input)) {
            return $this->unprocessableEntityResponse();
        }
        $this->studentsGateway->update($id, $input);
        $response['status_code_header'] = 'HTTP/1.1 200 OK';
        $response['body'] = null;
        return $response;
    }
    private function deleteById($id) {
        $result = $this->studentsGateway->find($id);
        if (! $result) {
            return $this->notFoundResponse();
        }
        $this->studentsGateway->delete($id);
        $response['status_code_header'] = 'HTTP/1.1 200 OK';
        $response['body'] = null;
        return $response;
    }
    private function validate($input) {
        if (! isset($input['firstname'])) {
            return false;
        }
        if (! isset($input['lastname'])) {
            return false;
        }
        return true;
    }
    private function unprocessableEntityResponse() {
        $response['status_code_header'] = 'HTTP/1.1 422 Unprocessable Entity';
        $response['body'] = json_encode([
            'error' => 'Invalid input'
        ]);
        return $response;
    }
    private function notFoundResponse() {
        $response['status_code_header'] = 'HTTP/1.1 404 Not Found';
        $response['body'] = null;
        return $response;
    }
}

Finally, create a /public/index.php file to serve as the front controller to process requests:

<?php
require "../bootstrap.php";

use Src\Controller\StudentsController;

header("Access-Control-Allow-Origin: *");
header("Content-Type: application/json; charset=UTF-8");
header("Access-Control-Allow-Methods: OPTIONS,GET,POST,PUT,DELETE");
header("Access-Control-Max-Age: 3600");
header("Access-Control-Allow-Headers: Content-Type, Access-Control-Allow-Headers, Authorization, X-Requested-With");  
 
$uri = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH);
$uri = explode( '/', $uri ); 
 
// all of our endpoints start with /students
// everything else results in a 404 Not Found
if ($uri[1] !== 'students') {
    header("HTTP/1.1 404 Not Found");
    exit();
} 
 
// the id is, of course, optional and must be a number:
$id = null;
if (isset($uri[2])) {
    $id = (int) $uri[2];
} 
 
$requestMethod = $_SERVER["REQUEST_METHOD"]; 
 
// pass the request method and user ID to the StudentsController and process the HTTP request:
$controller = new StudentsController($dbConnection, $requestMethod, $id);
$controller->processRequest();

Start the web server with the following command, where switch -t starts the server in the public folder:

php -S 127.0.0.1:8888 -t public

Test your API with postman with these endpoints:

return all recordsGET http://localhost:8888/students
return a specific recordGET http://localhost:8888/students/{id}
create a new recordPOST http://localhost:8888/students
update an existing recordPUT / http://localhost:8888/students/{id}
delete an existing recordDELETE http://localhost:8888/students/{id}


Sunday, February 2, 2025

Using PHP with AI models hosted on GitHub

Overview

In this article I will show you how you can experiment with AI models hosted on GitHub in a simple PHP web app. GitHub AI Models are intended for learning, experimentation and proof-of-concept activities. The feature is subject to various limits (including requests per minute, requests per day, tokens per request, and concurrent requests) and is not designed for production use cases.

Prerequisites

To proceed, you will need the following:

  • PHP - You need to have PHP version 8.3 (or higher) installed on your computer. You can download the latest version from https://www.php.net/downloads.php.
  • Composer – If you do not have Composer yet, download and install it for your operating system from https://getcomposer.org/download/.

Getting Started

There are many AI models from a variety of vendors that you can choose from. The starting point is to visit https://github.com/marketplace/models. At the time of writing, these are a subset of the models available:


For this article, I will use the "DeepSeek-R1" beside the red arrow above. If you click on that model, you will be taken to the model's landing page:

Click on the green "Get API key" button.


The first thing we need to do is get a 'personal access token' by clicking on the “Get developer key” button.

Choose 'Generate new token', which happens to be in beta at the time of writing.


Give your token a name, set the expiration, and optionally describe the purpose of the token. Thereafter, click on the green 'Generate token' button at the bottom of the page.


Copy the newly generated token and place it is a safe place because you cannot view this token again once you leave the above page.

Let's do some PHP coding

In a working directory, create a sub-directory named PHP-GitHub-AI inside a terminal window with the following command:

mkdir PHP-GitHub-AI

Change into the newly created directory named PHP-GitHub-AI with:

cd PHP-GitHub-AI

In the PHP-GitHub-AI folder, create a file named index.php and add to it the following code:

<?php

// Set your Azure API key and endpoint
$apiKey = 'PUT-YOUR-PERSONAL-ACCESS-TOKEN-FROM-GITHUB-HERE';
$endpoint = 'https://models.inference.ai.azure.com';

// Define the API endpoint
$url = $endpoint . '/chat/completions';

// Set up the data for the API request
$data = [
    'messages' => [
        [
            'role' => 'system',
            'content' => 'you are an expert in astronomy'
        ],
        [
            'role' => 'user',
            'content' => 'which is the furthest planet to earth?'
        ]
    ],
    'model' => 'DeepSeek-R1',    // gpt-4o   DeepSeek-R1
    'temperature' => 1,
    'max_tokens' => 4096,
    'top_p' => 1
];
// Initialize cURL
$ch = curl_init($url);

// Set cURL options
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
    'Content-Type: application/json',
    'Authorization: ' . $apiKey,
]);

curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($data));

// Execute the API request
$response = curl_exec($ch);

// Check for errors
if ($response === false) {
    echo 'Error: ' . curl_error($ch);
} else {

    // Decode the response
    $result = json_decode($response, true);

    // Print the entire response for debugging
    /*
    echo '<pre>';
    print_r($result);
    echo '</pre>';
    */
    // Check if the 'choices' key exists in the response
    if (isset($result['choices'][0]['message']['content'])) {
        echo '<h3>Generated Text by ' . $result['model'] .':</h3>';
        // Print the generated text
        echo "<p>" . $result['choices'][0]['message']['content'] . "</p>";
    } else {
        if (isset($result['error'])) {
            echo 'Error: ' . $result['error']['message'];
        } else {
            echo 'Error: Unable to retrieve generated text.';
        }
    }
}

// Close cURL

curl_close($ch);
?>

In the above code:

  • Set the value of $apiKey to be the personal access token from GitHub
  • The system prompt is: 'you are an expert in astronomy'.
  • The user prompt is: 'which is the furthest planet to earth?'
  • We will be using the ‘DeepSeek-R1’ model

You can start the PHP web server in the PHP-GitHub-AI folder with this terminal window command:

php -S localhost:8888

Point your browser to http://localhost:8888. The output would look like this:


The output is in markdown format. We will need a library that converts from markdown to HTML. To that end, stop the web server and install the erusev/parsedown package into your application with this terminal window command:

composer require erusev/parsedown

Back in the code, make the following changes:
  • Add this code to the first line of your PHP code:
// composer require erusev/parsedown
require_once 'vendor/autoload.php';

  • Replace the following statement:

echo "<p>" . $result['choices'][0]['message']['content'] . "</p>";

WITH

error_reporting(E_ALL ^ E_DEPRECATED);
$Parsedown = new Parsedown();
$text =  $Parsedown->text($result['choices'][0]['message']['content']);
echo "<p>$text</p>";

Restart the web server with “php -S localhost:8888”. The page now shows a much better looking output:

You can change the AI model from DeepSeek-R1 to any other model (like gpt-4o) and will get similar results.

Monday, January 20, 2025

Phi-3 Small Language Model (SLM) in a PHP app with Ollama and LLPhant framework

Overview

In this tutorial, we will see how easy it is to use the Phi-3 small language model in a PHP application. The best part is that it is free and runs entirely on your local device. Ollama is used to serve the Phi-3 small language model and LLPhant is the PHP framework for communicating with the AI model. 

Prerequisites

To proceed, you will need the following:

What is small language model (SLM)?

A small language model (SLM) is a machine learning model typically based on a large language model (LLM) but of greatly reduced size. An SLM retains much of the functionality of the LLM from which it is built but with far less complexity and computing resource demand.

What is Ollama?

Ollama is an application you can download onto your computer or server to run open-source generative AI small-language-models (SLMs) such as Meta's Llama 3 and Microsoft's Phi-3. You can see the many models available at https://www.ollama.com/library.

What is LLPhant

LLPhant is an open-source PHP Generative AI Framework at https://github.com/LLPhant/LLPhant

Getting Started

Download the Ollama installer from https://www.ollama.com/download.

Once you have installed Ollama, run these commands from a terminal window:

ollama pull phi3:latest
ollama list
ollama show phi3:latest

In a suitable working directory, create a folder named PhpAI with the following terminal window command:

mkdir PhpAI

Change into the newly created folder with:

cd PhpAI

Using Composer, install the theodo-group/llphant package by running this command:

composer require theodo-group/llphant

Let's get coding

Create a file named index.php with the following content:

<?php

require_once 'vendor/autoload.php';

use LLPhant\OllamaConfig;
use LLPhant\Chat\OllamaChat;  
 
$config = new OllamaConfig();
$config->model = 'phi3'; 
 
$chat = new OllamaChat($config); 
 
$chat->setSystemMessage('You are a helpful assistant who knows about world geography.'); 
 
$response = $chat->generateText('what is the capital of france?');  
 
echo $response;
?>

Running the app

To run the app, start the PHP web server to listen on port number 8888 with the following command in the PhpAI folder.

php -S localhost:8888

You can view the output by pointing your browser to the following URL:

http://localhost:8888/

This is what I experienced:

Conclusion

We can package our applications with a local SLM. This makes our applications cheaper, faster, connection-free, and self-contained.

Monday, January 15, 2024

PHP meets OpenAI with image generation

Let's generate images using OpenAI's dall-e-3 service. When using PHP, the open-source openai-php/client library facilitates the process. Check it out at https://github.com/openai-php/client.

In this article, we will learn how easy it is to generate an image with OpenAI and PHP. 

Source code: https://github.com/medhatelmasry/openai-dalle3-php

Prerequisites

In order to proceed, you will need the following:

  1. Subscription with OpenAI - If you do not have a subscription, go ahead and register at https://openai.com/.
  2. PHP - You need to have PHP version 8.2 (or higher) installed on your computer. You can download the latest version from https://www.php.net/downloads.php.
  3. Composer – If you do not have Composer yet, download and install it for your operating system from https://getcomposer.org/download/.

Getting Started

In a suitable working directory, create a folder named openai-dalle3-php with the following terminal window command:

mkdir openai-dalle3-php

Change into the newly created folder with:

cd openai-dalle3-php

Using Composer, install the openai-php/client package by running this command:

composer require openai-php/client

We will be reading our ApoenAPI key from the .env text file. We need to install this package in order to do that.

composer require vlucas/phpdotenv

Getting an API KEY from OpenAI

With your OpenAI credentials, login into https://openai.com/.  Click on API.

In the left navigation, click on "API keys". 


Click on the "+ Create new secret key" button.


Give the new key a name. I named it 20240115 representing the date when it was created. You may wish to give it a more meaningful or creative name. Once you are happy with the name, click on the "Create secret key" to generate the key.


Click on the copy button beside the key and paste the API-Key somewhere safe as we will need to use it later on. Note that you cannot view this key again. Click on Done to dismiss the dialog.

We will create a text file named .env in our  openai-dalle3-php folder with the following content:

OPENAI_API_KEY=put-your-openai-api-key-here

Set the API-Key as the value of OPENAI_API_KEY. This may look like this:

OPENAI_API_KEY=sk-OOghjTs8GsuHQklTCWOeT3BasdGJAjklBr3tr8ViZKv21BRN

Let's get coding

We can generate images by obtaining a URL to the newly created image, or by getting the Base-64 representation of the image. We will try both ways.

1) Get URL of the image

In the openai-dalle3-php folder, create a file named image-url.php with the following content:

<?php

require_once __DIR__ . '/vendor/autoload.php';

$dotenv = Dotenv\Dotenv::createImmutable(__DIR__);
$dotenv->load();
 
$client = OpenAI::client($_ENV['OPENAI_API_KEY']);

$response = $client->images()->create([
    'model' => 'dall-e-3',
    'prompt' => 'A panda flying over Paris at night',
    'n' => 1,
    'size' => '1024x1024',
    'response_format' => 'url',
]);

foreach ($response->data as $data) {
    $data->url; 
    $data->b64_json; // null
}

// display the image
echo '<img src="' . $data->url . '" />';

?>

In the above code, note the following:

  • we read in the API Key from .env file and pass it in the OpenAI::client($_ENV['OPENAI_API_KEY']); statement
  • we request a URL response with an image size 1024 x 1024
  • we prompt the dall-e-3 service to generate an image of 'A panda flying over Paris at night'.

To run the app, start the PHP web server to listen on port number 8888 with the following command in the openai-dalle3-php folder.

php -S localhost:8888

You can view the resulting image that gets created by OpenAI by pointing your browser to the following URL:

http://localhost:8888/image-url.php

This is the image that got created for me:


Every time you run the app you will likely get a different image.

2) Get Base64 encoding of the image

Create another file named image-b64.php with the following content:

    <?php

require_once __DIR__ . '/vendor/autoload.php';

$dotenv = Dotenv\Dotenv::createImmutable(__DIR__);
$dotenv->load();
 
$client = OpenAI::client($_ENV['OPENAI_API_KEY']);

$response = $client->images()->create([
    'model' => 'dall-e-3',
    'prompt' => 'A panda flying over Paris at night',
    'n' => 1,
    'size' => '1024x1024',
    'response_format' => 'b64_json',
]);

foreach ($response->data as $data) {
    $data->url; // null
    $data->b64_json; 
}

// display base64 encoded image
echo '<img src="data:image/jpeg;base64,' . $data->b64_json. '" />';

?>

The only changes that were made are in the following lines of code:

(1) 'response_format' => 'b64_json',

Whereas previously, we requested a URL, this time we request base-64 encoding.

(2) echo '<img src="data:image/jpeg;base64,' . $data->b64_json. '" />';

When rendering the image, we use base64 rendering instead of an image URL.

Point your browser to http://localhost:8888/image-b64.php. This is what I experienced:

Conclusion

There are may services that you can consume from OpenAP, like chat completion, text completion, embeddings, etc.. Now that you know how things work, go ahead and try some of the other services.

Saturday, March 26, 2022

Deploy DB driven Laravel web app to Azure App Service running on Linux NGINX web server through GitHub Actions

Laravel is an open-source PHP framework designed to make developing web apps mush easier and faster through built-in features. It is based on the MVC design pattern.

In this article, I will show you how to deploy a Laravel application to Azure App Services. The Azure web server is NGINX running on Linux version "Debian GNU/Linux 10 (buster)".

Assumptions

The following is assumed:

  • You have Git, PHP and Composer installed on your computer.
  • You have an Azure subscription
  • You have a GitHub account

Getting Started

We will start our journey with a very simple Laravel application that lists some data from a SQLite database. Go into a working directory on your computer and run the following command from a terminal window to clone a GitHub repo:

git clone https://github.com/medhatelmasry/laravel-azure.git

It is a good idea to delete the .git folder on your computer.

Once the application is cloned, change directory to the cloned app and start your app as shown below:

cd laravel-azure
composer install
php artisan serve --port=8888

The web server will start on your computer listening on port 8888.

Starting Laravel development server: http://127.0.0.1:8888
[Sat Mar 26 15:36:29 2022] PHP 8.1.2 Development Server (http://127.0.0.1:8888) started

Point your browser to http://localhost:8888/ and you will see the following page:


For demo purposes, data is pulled from a SQLite database file at database/autos.db

Disclaimer: It is normal practice that the .env file is excluded from being pushed to source control. If you look at the .gitignore file, the line pertaining to .env is commented out because there is really no confidential information in this file.

Create a repository in your GitHub account and push the source code to it.

Create Azure App Service

Login to your Azure account by visiting https://portal.azure.com. Enter "app services" in the filter field, then click on "App Services".



Click on "+ Create" on the top left-side.

On the "Create Web App Page", choose. your subscription then create new resource group.



Give your app a suitable host name that is unique.


Next, ensure these settings for Publish, Runtime stack and Operating System:


Choose suitable values for the remaining settings based on your individual preference then click on the blue "Review + create" button:


Click on Create button after you have reviewed your. choices.


Once your app service is successfully provisioned, you can click on the "Go to resource" button.


You can see a default web page to your web app be clicking on the URL link on the top right-side.


The default page looks like this.

\

CI/CD pipeline

Of course, we want to deploy our Laravel PHP site from our GitHub repo. An easy way to achieve this is to use Azure's "Deployment Center". Click on "Deployment Center" in the left-side navigation.

In the "Deployment Center" blade, select GitHub.


If this is your first time to connect Azure to your GitHub account, you will be asked to go through the GitHub authentication process. Thereafter, select the appropriate GitHub Organization, Repository and Branch. My experience was:


Once you click on Save, the deployment process commences.


To go to your GitHub repo, click on the main link under Branch.

To see the deployment in action, click on Actions in your GitHub repository.


Click on the workflow in progress.


The workflow will be going through a build and deploy process. 


Be patient as it takes about 15 minutes to complete. This is because during build the command "composer install" is executed. This produces a multitude of files under the vendors folder, which are thereafter sent to Azure. Once the workflow is completed, your GitHub workflow page will look like this:


The workflow files were automatically generated and placed a .yml file in the .github/workflows folder with your source code.


You can click on the .yml file to see what the file looks like.


At this point, it is worth going back to the code on your computer and doing a pull of your code so that you get a copy of the .yml file that was added to your source code.

Configuring app on Azure

Back on the Azure portal, if you refresh the default page of our web app, you will experience a "403 Forbidden" message, which typically means that the root directory has no welcome page.




This is understandable because the main index.php page in Laravel resides in the /public folder. THis means that we need to do some configuration work on Azure. 

In Azure, enter the term advanced in the search input field then click on "Advanced Tools".


Click "Go -->".


A page opens up in a new browser tab. Click on SSH on the top menu.


A Linux terminal window is open.



If you are interested to know what version of Linux this is, enter the following terminal command:

cat /etc/os-release

The value beside PRETTY_NAME is the Linux distribution and version.

PRETTY_NAME="Debian GNU/Linux 10 (buster)"
NAME="Debian GNU/Linux"
VERSION_ID="10"
VERSION="10 (buster)"
VERSION_CODENAME=buster
ID=debian
HOME_URL="https://www.debian.org/"
SUPPORT_URL="https://www.debian.org/support"
BUG_REPORT_URL="https://bugs.debian.org/"

We will make a copy of the existing nginx configuration and place the file inside the /home/site directory with the following command:

cp /etc/nginx/sites-available/default /home/site/default

Once copied, edit the /home/site/default file with nano or vi. I will use nano.

nano /home/site/default

Make the following changes:

FromToAround Line #
root /home/site/wwwrootroot /home/site/wwwroot/public6
location / {
   index  index.php index.html index.htm hostingstart.html;
}
      
location / {
   index  index.php index.html index.htm hostingstart.html;
   try_files $uri $uri/ /index.php?$query_string;
}
      
10


In nano, hit CTRL X to save, enter Y then hit return.

We need to create a bash script file that overrides the existing default file with our customized version, then restart the server. Change directory to the site folder.

cd site

Inside the site directory, using vi or nano, create a file named startup.sh. To create a file with nano, type "nano startup.sh". Otherwise,  to create a file with vi, type "vi startup.sh". Add the following content to startup.sh:

#!/bin/bash

cp /home/site/default /etc/nginx/sites-available/default
service nginx reload

We will make our bash script file executable with:

chmod u+x startup.sh

While in the terminal window on Azure, let's visit the folder that contains our Laravel application. Do this by going to the wwwroot folder.

cd wwwroot
ls -a

This reveals the existence of all the files that we had on GitHub plus files in the vendor folder.


Navigate back to your App Service via the Azure Portal. Select Configuration in the Settings section.


Click on the "General Settings" tab, enter "/home/site/startup.sh" for the "Startup Command", then click on Save.


Click on blue Continue button the the "Save changes" prompt.


Now, when you refresh the website, you should see that our web app is working as expected.

Conclusion

We have successfully deployed a database driven Laravel PHP application to Azure App Services through GitHub. I hope you use the principles you learned in this tutorial to deploy much more interesting Laravel applications.