Skip to content

Commit

Permalink
Initial commit: Add Docker setup and PHP application files
Browse files Browse the repository at this point in the history
  • Loading branch information
BhavikaPachauri committed Jul 28, 2024
0 parents commit c3c0734
Show file tree
Hide file tree
Showing 14 changed files with 593 additions and 0 deletions.
17 changes: 17 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# Use an official PHP image as the base image
FROM php:8.0-apache

# Install necessary PHP extensions and other dependencies
RUN docker-php-ext-install pdo pdo_mysql

# Copy application code to the working directory
COPY . /var/www/html

# Set the working directory
WORKDIR /var/www/html

# Expose port 80
EXPOSE 80

# Start the Apache server
CMD ["apache2-foreground"]
108 changes: 108 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
# PHP Developer Test

Hello and thanks for taking the time to try this out.

The goal of this test is to assert (to some degree) your coding and architectural skills. You're given a simple problem so you can focus on showcasing development techniques. We encourage you to overengineer the solution to show off what you can do - assume you're building a production-ready application that other developers will need to work on and add to over time.

You're **allowed and encouraged** to use third party libraries, as long as you put them together yourself **without relying on a framework or microframework** to do it for you. An effective developer knows what to build and what to reuse, but also how his/her tools work. Be prepared to answer some questions about those libraries, like why you chose them and what other alternatives you're familiar with.

As this is a code review process, please avoid adding generated code to the project. This makes our jobs as reviewers more difficult, as we can't review code you didn't write. This means avoiding libraries like _Propel ORM_, which generates thousands of lines of code in stub files.


## Prerequsites

We use [Docker](https://www.docker.com/products/docker) to administer this test. This ensures that we get an identical result to you when we test your application out, and it also matches our internal development workflows. If you don't have it already, you'll need Docker installed on your machine. **The application MUST run in the Docker containers** - if it doesn't we cannot accept your submission. You **MAY** edit the containers or add additional ones if you like (or completely re-do everything), but this **MUST** be clearly documented.

We have provided some containers to help build your application in PHP with a variety of persistence layers available to use. (you may start from scratch if you like)

### Technology

- Valid PHP 7.1 or newer
- Persist data to either Postgres, Mysql (add yourself), Redis, or MongoDB (in the provided containers).
- Postgres connection details:
- host: `postgres`
- port: `5432`
- dbname: `hellofresh`
- username: `hellofresh`
- password: `hellofresh`
- Redis connection details:
- host: `redis`
- port: `6379`
- MongoDB connection details:
- host: `mongodb`
- port: `27017`
- Use the provided `docker-compose.yml` file in the root of this repository. You are free to add more containers to this if you like.

## Instructions

1. Create a Git Repository and add these files
- Run `docker-compose up -d` to start the development environment.
- Visit `http://localhost` to see the contents of the web container and develop your application.
- Add all code changes to the git repository
- Zip all completed files (with the git repository files) and email back to us.

## Requirements

We'd like you to build a simple Recipes API. The API **MUST** conform to REST practices and **MUST** provide the following functionality:

- List, create, read, update, and delete Recipes
- Search recipes
- Rate recipes

### Endpoints

Your application **MUST** conform to the following endpoint structure and return the HTTP status codes appropriate to each operation. Endpoints specified as protected below **SHOULD** require authentication to view. The method of authentication is up to you.

##### Recipes

| Name | Method | URL | Protected |
| --- | --- | --- | --- |
| List | `GET` | `/recipes` ||
| Create | `POST` | `/recipes` ||
| Get | `GET` | `/recipes/{id}` ||
| Update | `PUT/PATCH` | `/recipes/{id}` ||
| Delete | `DELETE` | `/recipes/{id}` ||
| Rate | `POST` | `/recipes/{id}/rating` ||

An endpoint for recipe search functionality **MUST** also be implemented. The HTTP method and endpoint for this **MUST** be clearly documented.

### Schema

- **Recipe**
- Unique ID
- Name
- Prep time
- Difficulty (1-3)
- Vegetarian (boolean)

Additionally, recipes can be rated many times from 1-5 and a rating is never overwritten.

If you need a more visual idea of how the data should be represented, [take a look at one of our recipe cards](https://ddw4dkk7s1lkt.cloudfront.net/card/hdp-chicken-with-farro-75b306ff.pdf?t=20160927003916).

## Evaluation criteria

These are some aspects we pay particular attention to:

- You **MUST** use packages, but you **MUST NOT** use a web-app framework or microframework. That is, you can use [symfony/dependency-injection](https://packagist.org/packages/symfony/dependency-injection) but not [symfony/symfony](https://packagist.org/packages/symfony/symfony).
- Your application **MUST** run within the containers. Please provide short setup instructions.
- The API **MUST** return valid JSON and **MUST** follow the endpoints set out above.
- You **MUST** write testable code and demonstrate unit testing it (for clarity, PHPUnit is not considered a framework as per the first point above. We encourage you to use PHPUnit or any other kind of **testing** framework).
- You **SHOULD** pay attention to best security practices.
- You **SHOULD** follow SOLID principles where appropriate.
- You do **NOT** have to build a UI for this API.

The following earn you bonus points:

- Your answers during code review
- An informative, detailed description in the PR
- Setup with a one liner or a script
- Content negotiation
- Pagination
- Using any kind of Database Access Abstraction
- Other types of testing - e.g. integration tests
- Following the industry standard style guide for the language you choose to use - `PSR-2` etc.
- A git history (even if brief) with clear, concise commit messages.

---

Good luck!
28 changes: 28 additions & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
version: '3.7'

services:
web:
build:
context: .
dockerfile: Dockerfile
ports:
- "8080:80"
volumes:
- .:/var/www/html
depends_on:
- db

db:
image: mysql:5.7
environment:
MYSQL_ROOT_PASSWORD: root
MYSQL_DATABASE: recipe_db
MYSQL_USER: user
MYSQL_PASSWORD: password
ports:
- "3306:3306"
volumes:
- db_data:/var/lib/mysql

volumes:
db_data:
38 changes: 38 additions & 0 deletions docker/nginx/default.conf
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
server {
listen 80;

root /server/http/web;
index index.html index.htm index.php;

access_log off;
error_log /var/log/nginx/error.log error;

charset utf-8;

location / {
try_files $uri $uri/ /index.php?$query_string;
}

location = /favicon.ico { log_not_found off; access_log off; }
location = /robots.txt { access_log off; log_not_found off; }

sendfile off;

client_max_body_size 100m;

location ~ \.php$ {
fastcgi_split_path_info ^(.+\.php)(/.+)$;
fastcgi_pass unix:/run/php/php7.1-fpm.sock;
fastcgi_index index.php;
include fastcgi_params;
include fastcgi.conf;
fastcgi_intercept_errors off;
fastcgi_buffer_size 16k;
fastcgi_buffers 4 16k;
}

# Deny .htaccess file access
location ~ /\.ht {
deny all;
}
}
37 changes: 37 additions & 0 deletions web/create.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Create Recipe</title>
<link href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-T3c6CoIi6uLrA9TneNEoa7RxnatzjcDSCmG1MXxSR1GAsXEV/Dwwykc2MPK8M2HN" crossorigin="anonymous">
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/js/bootstrap.bundle.min.js" integrity="sha384-C6RzsynM9kWDrMNeT87bh95OGNyZPhcTNXj1NW7RuBCsyN/o0jlpcV8Qyq46cDfL" crossorigin="anonymous"></script>
</head>
<body>
<div class="container">
<h2 class="text-center">Create Recipe</h2>
<form action="create_action.php" method="POST">
<div class="mb-3">
<label for="name" class="form-label">Recipe Name</label>
<input type="text" class="form-control" id="name" name="name" required>
</div>
<div class="mb-3">
<label for="prep_time" class="form-label">Preparation Time</label>
<input type="text" class="form-control" id="prep_time" name="prep_time" required>
</div>
<div class="mb-3">
<label for="difficulty" class="form-label">Difficulty</label>
<input type="text" class="form-control" id="difficulty" name="difficulty" required>
</div>
<div class="mb-3">
<label for="vegetarian" class="form-label">Vegetarian</label>
<select class="form-control" id="vegetarian" name="vegetarian" required>
<option value="1">Yes</option>
<option value="0">No</option>
</select>
</div>
<button type="submit" class="btn btn-primary">Create</button>
</form>
</div>
</body>
</html>
21 changes: 21 additions & 0 deletions web/create_action.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<?php
include 'db.php';

$name = $_POST['name'];
$prep_time = $_POST['prep_time'];
$difficulty = $_POST['difficulty'];
$vegetarian = $_POST['vegetarian'];

$sql = "INSERT INTO recipe (name, prep_time, difficulty, vegetarian) VALUES (?, ?, ?, ?)";
$stmt = $conn->prepare($sql);
$stmt->bind_param("sssi", $name, $prep_time, $difficulty, $vegetarian);

if ($stmt->execute()) {
header("Location: index.php");
} else {
echo "Error: " . $sql . "<br>" . $conn->error;
}

$stmt->close();
$conn->close();
?>
14 changes: 14 additions & 0 deletions web/db.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<?php
$servername = "db";
$username = "user";
$password = "password";
$dbname = "recipe_db";

// Create connection
$conn = new mysqli($servername, $username, $password, $dbname);

// Check connection
if ($conn->connect_error) {
die("Connection failed: " . $conn->connect_error);
}
?>
18 changes: 18 additions & 0 deletions web/delete.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<?php
include 'db.php';

$id = $_POST['id'];

$sql = "DELETE FROM recipe WHERE id = ?";
$stmt = $conn->prepare($sql);
$stmt->bind_param("i", $id);

if ($stmt->execute()) {
header("Location: index.php");
} else {
echo "Error: " . $sql . "<br>" . $conn->error;
}

$stmt->close();
$conn->close();
?>
107 changes: 107 additions & 0 deletions web/index.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Recipe CRUD APP</title>
<link href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-T3c6CoIi6uLrA9TneNEoa7RxnatzjcDSCmG1MXxSR1GAsXEV/Dwwykc2MPK8M2HN" crossorigin="anonymous">
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/js/bootstrap.bundle.min.js" integrity="sha384-C6RzsynM9kWDrMNeT87bh95OGNyZPhcTNXj1NW7RuBCsyN/o0jlpcV8Qyq46cDfL" crossorigin="anonymous"></script>
</head>
<body>
<div class="container">
<h2 class="text-center">Recipe CRUD APP</h2>

<form method="GET" action="index.php" class="d-flex mb-4">
<input class="form-control me-2" type="search" name="search" placeholder="Search by name" value="<?php echo isset($_GET['search']) ? $_GET['search'] : ''; ?>">
<button class="btn btn-outline-success" type="submit">Search</button>
</form>

<a href="create.php" class="btn btn-primary mb-3">Create</a>

<table class="table table-hover">
<thead>
<tr>
<th scope="col">ID</th>
<th scope="col">Name</th>
<th scope="col">Prep Time</th>
<th scope="col">Difficulty</th>
<th scope="col">Vegetarian</th>
<th scope="col">Actions</th>
</tr>
</thead>
<tbody>
<?php
include 'db.php';

// Search functionality
$search = isset($_GET['search']) ? $_GET['search'] : '';

// Pagination functionality
$limit = 5; // Number of entries per page
$page = isset($_GET['page']) ? $_GET['page'] : 1;
$offset = ($page - 1) * $limit;

// SQL query with search and pagination
$sql = "SELECT * FROM recipe WHERE name LIKE ? LIMIT ?, ?";
$stmt = $conn->prepare($sql);
$searchParam = '%' . $search . '%';
$stmt->bind_param('sii', $searchParam, $offset, $limit);
$stmt->execute();
$result = $stmt->get_result();

if ($result->num_rows > 0) {
while ($row = $result->fetch_assoc()) {
?>
<tr>
<th scope="row"><?php echo $row["id"]; ?></th>
<td><?php echo $row["name"]; ?></td>
<td><?php echo $row["prep_time"]; ?></td>
<td><?php echo $row["difficulty"]; ?></td>
<td><?php echo $row["vegetarian"] ? 'Yes' : 'No'; ?></td>
<td>
<a href="read.php?id=<?php echo $row['id']; ?>"><button class="btn btn-info">READ</button></a>
<a href="update.php?id=<?php echo $row['id']; ?>"><button class="btn btn-warning">EDIT</button></a>
<form action="delete.php" method="POST" style="display:inline;" onsubmit="return confirm('Are you sure you want to delete this item?');">
<input type="hidden" name="id" value="<?php echo $row['id']; ?>">
<button type="submit" class="btn btn-danger">DELETE</button>
</form>
<a href="rate.php?id=<?php echo $row['id']; ?>"><button class="btn btn-success">RATE</button></a>
</td>
</tr>
<?php
}
} else {
echo "<tr><td colspan='6' class='text-center'>No records found</td></tr>";
}
$stmt->close();

// Total records for pagination
$sql_total = "SELECT COUNT(*) as count FROM recipe WHERE name LIKE ?";
$stmt_total = $conn->prepare($sql_total);
$stmt_total->bind_param('s', $searchParam);
$stmt_total->execute();
$result_total = $stmt_total->get_result();
$total = $result_total->fetch_assoc()['count'];
$stmt_total->close();

$conn->close();
?>
</tbody>
</table>

<!-- Pagination Links -->
<?php
$total_pages = ceil($total / $limit);
if ($total_pages > 1) {
echo '<nav>';
echo '<ul class="pagination justify-content-center">';
for ($i = 1; $i <= $total_pages; $i++) {
echo '<li class="page-item ' . ($i == $page ? 'active' : '') . '"><a class="page-link" href="index.php?page=' . $i . '&search=' . $search . '">' . $i . '</a></li>';
}
echo '</ul>';
echo '</nav>';
}
?>
</div>
</body>
</html>
Loading

0 comments on commit c3c0734

Please sign in to comment.