I Built my First PHP App

It took about 2 working weeks to finish the app. I made 4 iterations of the app, and each duration mentioned on each step reflects this. Most of my time went into styling the application since I opted to use raw CSS. The hosted solution can be found here: https://expensify.fly.dev/

The project was quite simple to start since the project development steps were pretty much predetermined.

Step 1: Hosting basic PHP environment

Duration: Approximately 2 hours

Since I’m a MacOS user, installing PHP was quite straightforward:

# don't skip this otherwise, you'll be stuck for hours trying to figure out what the bash errors mean. 😢 
brew update
brew install

I used the PHP Server VScode extension to serve my app and PHP DEVSENSE VScode extension for PHP development tools like code completion.

Step 2: Understanding Tech Stack constraints and building PHP proxy file.

Duration: Approximately 4 days

I liked that my choices were narrowed down for me. Otherwise, I would’ve been paralyzed by the tons of choices not only in the JavaScript ecosystem but also in PHP.

This is the tech stack I used for the app:

Building the PHP proxy file took longer than I’d liked:

// proxy.php

<?php

// 1.
$url = $_REQUEST["url"];

if (!$url) {
    echo "You need to pass in a target URL.";
    return;
}

// 2.
$response = "";
switch (getMethod()) {
    case 'POST':
        $response = makePostRequest(getPostData(), $url);
        break;
    case 'GET':
        $response = makeGetRequest(getGetData($url), $url);
        break;
    default:
        echo "This proxy only supports POST AND GET REQUESTS.";
        return;
}

echo $response;

function getMethod()
{
    return $_SERVER["REQUEST_METHOD"];
}

// 3.
function makePostRequest($data, $url)
{
    $httpHeader = array(
        'Content-Type: application/json',
        'Content-Length: ' . strlen($data)
    );

    return makePostCurl('POST', $data, true, $httpHeader, $url);
}

// 4.
function makeGetRequest($data, $url)
{
    $ch = initCurl($url);
    curl_setopt($ch, CURLOPT_URL, $data);
    curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);

    $response = curl_exec($ch);
    curl_close($ch);

    return $response;
}

// 5.
function getPostData()
{
    return http_build_query($_POST);
}

// 6.
function getGetData($url)
{
    $daurl = http_build_query($_GET);
    $daurl = urldecode($daurl);

    return substr($daurl, 4);
}

// 7.
function initCurl($url)
{

    $httpHeader = array(
        'Content-Type: application/x-www-form-urlencoded'
    );

    $ch = curl_init($url);
    curl_setopt($ch, CURLOPT_HTTPHEADER, $httpHeader);
    curl_setopt($ch, CURLOPT_USERAGENT, 'Mozilla/5.0 (Windows NT 6.2; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/32.0.1667.0 Safari/537.36');

    return $ch;
}

// 8.
function makePostCurl($type, $data, $returnTransfer, $httpHeader, $url)
{
    $ch = initCurl($url);
    curl_setopt($ch, CURLOPT_CUSTOMREQUEST, $type);
    curl_setopt($ch, CURLOPT_POSTFIELDS, $data);
    curl_setopt($ch, CURLOPT_RETURNTRANSFER, $returnTransfer);

    $response = curl_exec($ch);
    curl_close($ch);
    return $response;
}

?>
  1. Retrieves the target URL from the query string parameter, url.
  2. Determines the request method of the current HTTP request through the getMethod() helper function.
  3. If it’s a POST request, it calls the makePostRequest() function, which sends the POST data to the target URL using cURL.
  4. If it’s a GET request, it calls the makeGetRequest() function, which sends the GET data to the target URL using cURL.
  5. The getPostData() function retrieves the POST data as a URL-encoded string.
  6. The getGetData() function retrieves the GET data as a string and removes the initial “data=” substring.
  7. The initCurl() function initializes a cURL handle with the URL and sets the HTTP headers and user agent. The response from the target URL is returned and printed to the client’s browser.
  8. The makePostCurl() function creates a cURL handle, sets the request method to POST, sets the POST data, sets the return transfer option, sets the HTTP headers, and executes the request.

Duration: Approximately 6 hours

On page load, there’s a check to confirm if the authToken cookie exists. If one exists, the user views the transactions page (step 6), if not, the user is presented with a login form.

 <!-- index.php -->
<body>
    <!-- Show login form if authToken cookie is expired -->
    <?php
    if (!isset($_COOKIE['authToken'])) {
        include "login.php";
    } else {
        include "transactions.php";
    }
    ?>

    <script type="text/javascript" src="https://ajax.googleapis.com/ajax/libs/jquery/3.1.0/jquery.min.js"></script>
    <script type="text/javascript" src="script.js"></script>
</body>

Step 4: Creating Log in form

Duration: Approximately 10 hours

The login form reads your email and password information.

 
<!-- login.php -->
<body>
    <div id="login-content" class="login-form">
        <!-- Login Form-->
        <form method="POST" id="login-form">
            <h1>
                Login
            </h1>
            <div class="content">
                <div class="input-field">
                    <input type="email" placeholder="Email" id="user-email" value="expensifytest@mailinator.com"
                        required />
                </div>
                <div class="input-field">
                    <input type="password" placeholder="Password" id="user-password" value="hire_me" required />
                </div>
                <a href="#" class="link">Forgot Your Password?</a>
            </div>
            <div class="action">
                <button id="cancel-login">Cancel</button>
                <button type="submit">Login</button>
            </div>
        </form>
    </div>

		<script type="text/javascript" src="script.js"></script>
    <script>

        // handle click on login cancel button
        $("#cancel-login").click((e) => {
            e.preventDefault()
            $("#login-form").trigger("reset");
        })
    </script>
</body>
Login Screenshot
// script.js

$(document).ready(() => {
    // submit user login info
    $('#login-form').submit((e) => {
        e.preventDefault();

				// 1.
        const partnerName = "applicant";
        const partnerPassword = "d7c3119c6cdab02d68d9";

        let partnerUserID = $('#user-email').val();
        let partnerUserSecret = $('#user-password').val();

        //store all the submitted data in a string.
        let formData = 'partnerName=' + partnerName + '&partnerPassword=' + partnerPassword + '&partnerUserID=' + partnerUserID + '&partnerUserSecret=' + partnerUserSecret;
        let authToken, jCode
        let url = "proxy.php?url=https://www.expensify.com/api?command=Authenticate"

        const emailRegex = /^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$/g;

				// 2.
        if (!partnerUserID.match(emailRegex)) {
            alert("Please Enter a Valid Email!");
            return false;
        }

				// 3.
        else {
            $.ajax(url, {
                type: "POST",
                data: formData,
                dataType: "json",
            }).done((res) => {
                authToken = res.authToken
                jCode = res.jsonCode

								//4.
                if (jCode == 200) {
                    document.cookie = `authToken=${authToken}; max-age=3600; `
                    alert("⚠️ Reload this window to view all your transactions.")
                } else {
                    ErrorHandler(jCode, res.message)
                }
            })
        }
    });
});

When the login form is submitted:

  1. API and user account credentials are captured and stored as a query string.
  2. Checks of the user’s email is valid.
  3. AJAX makes a POST request to the Authenticate endpoint.
  4. If the request is successful, the authToken retrieved is set in cookie storage. If not, the ErrorHandler function is called for the next error-handling step to execute.

Step 5: Handle Error messages

Duration: Approximately 2 hours

The ErrorHandler function outputs meaningful alert messages for any errors received should an API request fail.

// script.js

// handle common error codes
const ErrorHandler = (jCode, msg) => {
    let message = ""
    switch (jCode) {
        case 401:
            message = 'Wrong Password! Please enter the correct password.'
            break
        case 404:
            message = 'Account not found! Please enter a valid email.'
            break
        case 407:
            message = "Your session has expired. Please sign in again."
            break
        default:
            message = msg
    }
    alert(message)
}

Step 6: Fetch all transactions

Duration: Approximately 6 hours

Transactions are fetched and added to the table body.

<!-- transactions.php -->

<div id="transaction-table" class="transaction-table">
    <div class="transaction-head">
        <h2>Transactions:</h2>
        <button role="button" id="create-transaction-btn">+ Create Transaction</button>
    </div>

    <table>
        <thead>
            <tr>
                <th>Transaction Date</th>
                <th>Merchant</th>
                <th>Amount ($)</th>
            </tr>
        </thead>

<!-- transaction rows are added here -->
        <tbody id="transaction-table-body">

        </tbody>
    </table>
</div>
Transactions Screenshot
// script.js

// Hydrate transaction table with data
// 1.
const BuildTransactionTable = (data) => {
    let url = "proxy.php?url=https://www.expensify.com/api?command=Get"
    let table = $('#transaction-table-body')
    let transactions, jCode

		// 2.
    $.ajax(url, {
        data,
        dataType: "json",
    }).done((res) => {
        transactions = res.transactionList
        jCode = res.jsonCode

				// 3.
        if (jCode == 200) {
            for (let i = 0; i < transactions.length; i++) {
                let transactionCost = parseFloat(transactions[i].amount / 100).toFixed(2)
                transactionCost = new Intl.NumberFormat('en-US', { currency: 'USD' }).format(transactionCost)
                let row = `<tr>
                <td>${transactions[i].created}</td>
                <td>${transactions[i].merchant}</td>
                <td>${transactionCost}</td>
              </tr>`
                table.append(row)
            }
        } else {
            ErrorHandler(jCode, res.message)
        }
    });
}

The BuildTransactionTable function handles how the transaction table rows are generated.

  1. Receives data argument that’s the data query string required to make a request.
  2. Makes a GET request using AJAX to the Get endpoint.
  3. If the request is successful, it populates the transaction table with rows of all transaction data. Otherwise, the ErrorHandler function displays an error message.

Step 7: Manipulating transactions displayed in the table

Duration: Approximately 5 hours

The transactions fetched from the previous step are a hustle to go through. Therefore, I added a simple input form on top of the table to help navigate the transactions.

<!-- transactions.php -->

<div id="transaction-table" class="transaction-table">
    <div class="transaction-head">
        <h2>Transactions:</h2>
        <button role="button" id="create-transaction-btn">+ Create Transaction</button>
    </div>

<!-- Simple form to control transaction rows displayed -->
    <form id="show-form">
        <table>
            <tbody>
                <tr>
                    <td>
                        <label for="start-date">From:</label>
                        <input id="start-date" required type="date" name="start-date" placeholder="Start Date" />
                    </td>
                    <td>
                        <label for="end-date">To:</label>
                        <input id="end-date" required type="date" name="end-date" placeholder="End Date" />
                    </td>
                    <td>
                        <button type="submit" id="show-button">Show</button>
                        <button type="button" id="show-all">Show All</button>
                        <button type="button" id="cancel-show">Cancel</button>
                    </td>
                </tr>
            </tbody>
        </table>
    </form>
    <table>
        <thead>
            <tr>
                <th>Transaction Date</th>
                <th>Merchant</th>
                <th>Amount ($)</th>
            </tr>
        </thead>

        <tbody id="transaction-table-body">

        </tbody>
    </table>
</div>

<script type="text/javascript" src="https://ajax.googleapis.com/ajax/libs/jquery/3.1.0/jquery.min.js"></script>
<script type="text/javascript" src="script.js"></script>
<script>
    // Handle Create Transaction modal
    let modal = $("#create-transaction");
    let btn = $("#create-transaction-btn");
    let span = $(".close");
    let cancel = $("#cancel-btn")

    // open the modal 
    btn.click(() => {
        modal.css("display", "block")
    })

    // close the modal
    span.click(() => {
        modal.css("display", "none")
    })

    cancel.click(() => {
        modal.css("display", "none")
    })

    // When the user clicks anywhere outside of the modal, close it
    $(document).click((e) => {
        if ($(e.target).is('#create-transaction')) {
            modal.css("display", "none")
        }
    })
</script>
Screenshot of filtered transactions
// script.js

// fetch all transactions to populate the transactions table
const GetTransactions = () => {
    let table = $('#transaction-table-body')

    let showBtn = $("#show-button")
    let showAllBtn = $("#show-all")
    let cancelShowBtn = $("#cancel-show")
    let transactionParams = cookieToken + '&returnValueList=transactionList'

		// 1.
    BuildTransactionTable(transactionParams)

		// 2.
    showBtn.click((e) => {
        e.preventDefault()
        let startDate = $("#start-date").val()
        let endDate = $("#end-date").val()

        table.empty()
        if (startDate != null || startDate != undefined || endDate != null || endDate != undefined) {
            transactionParams = cookieToken + '&returnValueList=transactionList' + '&startDate=' + startDate + '&endDate=' + endDate
            BuildTransactionTable(transactionParams)
        } else {
            transactionParams = cookieToken + '&returnValueList=transactionList'
            BuildTransactionTable(transactionParams)
        }
    })

		// 3.
    showAllBtn.click((e) => {
        e.preventDefault()

        table.empty()
        transactionParams = cookieToken + '&returnValueList=transactionList'
        BuildTransactionTable(transactionParams)
    })

		// 4.
    cancelShowBtn.click((e) => {
        e.preventDefault()
        $("#show-form").trigger("reset");
    })
}
  1. The function calls the BuildTransactionTable function by default to populate the transactions table with all transactions fetched.
  2. When the Show button is clicked, it only displays transactions between the specified start and end date.
  3. When the Show All button is clicked, it displays all transactions.
  4. When the Cancel button is clicked, it resets the date input fields.

Step 8: Create a new transaction form

Duration: Approximately 2 hours

The + Create Transaction button on top of the transactions table displays a form modal to create a new transaction.

<!-- transactions.php -->

<div class="login-form modal" id="create-transaction">
    <!-- Create transaction form here -->
    <form method="POST" id="transaction-form" class="modal-content">
        <div class="modal-header">
            <h1>Create Transaction</h1>
            <span class="close">&times;</span>
        </div>
        <div class="content">
            <div class="input-field">
                <input type="date" placeholder="Transaction date" id="transaction-date" required />
            </div>
            <div class="input-field">
                <input type="string" placeholder="Merchant" id="merchant" required />
            </div>
            <div class="input-field">
                <input type="number" placeholder="Amount" id="transaction-amount" required />
            </div>
        </div>
        <div class="action">
            <button id="cancel-btn">Cancel</button>
            <button type="submit">Add</button>
        </div>
    </form>
</div>
Create Transaction Screenshot
// script.js

$(document).ready(() => {
		// create transaction form
    $('#transaction-form').submit((e) => {
        e.preventDefault();
        let transactionDate = $('#transaction-date').val();
        let transactionAmount = $('#transaction-amount').val();
        let merchant = $('#merchant').val();
        let url = "proxy.php?url=https://www.expensify.com/api?command=CreateTransaction"
        let createTransaction = cookieToken + '&created=' + transactionDate + '&amount=' + transactionAmount + '&merchant=' + merchant;
        let jsCode

				// 1.
        $.ajax(url, {
            type: "POST",
            data: createTransaction,
            dataType: "json",
        }).done((res) => {
            jsCode = res.jsonCode

						// 2.
            if (jsCode == 200) {
                alert("Transaction Saved ✅")
                $(".alert").css("display", "block")
                $("#create-transaction").css("display", "none")

						// 3.
            } else {
                ErrorHandler(jsCode, res.message)
            }
        });
    })
});
  1. When the create transaction form is submitted, it sends a POST request using AJAX to the CreateTransaction endpoint for the new transaction to be added.
  2. If the request is successful, the function sends an alert that the transaction has been saved. And also, notifies the user to reload their window to display the updated transactions (step 9).
  3. If the request fails, the ErrorHandler function displays a useful error message.

Step 9: Prompt user to refresh page

Duration: Approximately 2 hours

To prove that a new transaction was created, the user is notified to reload the page to display an updated transition table.

<!-- transactions.php -->

<div class="alert">
    <span id="alert_box" class="closebtn" onclick="this.parentElement.style.display='none';">&times;</span>
    ⚠️ Reload this window to view the updated transaction list.
</div>
Screenshot with warning banner

Step 10: Hosting the app

Duration: Approximately 3 days

I struggled to host the PHP app. I’ve never tried hosting an web app that:

  1. Needs a live server to run.
  2. Isn’t uploaded on a GitHub repo.

I first tried using AWS Cloud9 and an EC2. All Youtube tutorials I found on PHP hosting on an EC2 instance were outdated and the docs kept leading me through an infinite rabbit hole of pre requisite steps to be accomplished. It was too frustrating since my app is dynamic. Using Cloud9 was less complicated than using an EC2 instance but I found another alternative that worked better for me.

I threw my app into a Docker container and used fly.io to deploy the app after importing my code files.

Challenges encountered

I went through 4 full iterations of my app rectifying my code and UX designs before coming up with the final product. Here are the major challenges I experienced:

  1. Learning curve: Being new to PHP, I was initially overwhelmed by it; trying to understand how it will fit into the general structure of the app. Basically where JavaScript started and PHP ended. I used some written PHP courses and a ton of Googling to find my way out of unfamiliar errors.

  2. Building the PHP proxy script: Since I was unfamiliar with this, I googled the heck out of it to fully understand its purpose. I got stuck for 2 days trying to figure out why my GET requests were responding with some missing information. I used Hoppscotch to narrow down the source of my problem. Fortunately, Hoppscotch allows you to get raw PHP curl code when you make a request. I realized that my URL string was off and I fixed it.

  3. Building a good user experience, improving maintainability and readability: I thought about how I wanted my designs to look - simple and appealing. I searched for inspiration from the design gods on dribble.com and got a gist of what I wanted.

    I also used consistent and definitive naming conventions. I broke down complex logic into smaller functions, used meaningful error messages, commented on my code and kept functions as short as possible,

Conclusion

I had fun overcoming my PHP fears and biases; I always love a good challenge. If I were to further develop this application, my initial priorities would be increasing feedback on the UI, and optimizing the design for mobile devices.

Sonia Lomo

© 2026 Sonia Lomo

LinkedIn 𝕏 GitHub