diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..224e0f7 --- /dev/null +++ b/Dockerfile @@ -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"] diff --git a/README.md b/README.md new file mode 100644 index 0000000..b9e4a5a --- /dev/null +++ b/README.md @@ -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! diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..6bd4eba --- /dev/null +++ b/docker-compose.yml @@ -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: diff --git a/docker/nginx/default.conf b/docker/nginx/default.conf new file mode 100644 index 0000000..0b6fea5 --- /dev/null +++ b/docker/nginx/default.conf @@ -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; + } +} diff --git a/web/create.php b/web/create.php new file mode 100644 index 0000000..5790463 --- /dev/null +++ b/web/create.php @@ -0,0 +1,37 @@ + + + + + + Create Recipe + + + + +
+

Create Recipe

+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+ +
+
+ + diff --git a/web/create_action.php b/web/create_action.php new file mode 100644 index 0000000..be1fdcd --- /dev/null +++ b/web/create_action.php @@ -0,0 +1,21 @@ +prepare($sql); +$stmt->bind_param("sssi", $name, $prep_time, $difficulty, $vegetarian); + +if ($stmt->execute()) { + header("Location: index.php"); +} else { + echo "Error: " . $sql . "
" . $conn->error; +} + +$stmt->close(); +$conn->close(); +?> diff --git a/web/db.php b/web/db.php new file mode 100644 index 0000000..0b545cd --- /dev/null +++ b/web/db.php @@ -0,0 +1,14 @@ +connect_error) { + die("Connection failed: " . $conn->connect_error); +} +?> diff --git a/web/delete.php b/web/delete.php new file mode 100644 index 0000000..3f16b97 --- /dev/null +++ b/web/delete.php @@ -0,0 +1,18 @@ +prepare($sql); +$stmt->bind_param("i", $id); + +if ($stmt->execute()) { + header("Location: index.php"); +} else { + echo "Error: " . $sql . "
" . $conn->error; +} + +$stmt->close(); +$conn->close(); +?> diff --git a/web/index.php b/web/index.php new file mode 100644 index 0000000..66a960f --- /dev/null +++ b/web/index.php @@ -0,0 +1,107 @@ + + + + + + Recipe CRUD APP + + + + +
+

Recipe CRUD APP

+ +
+ + +
+ + Create + + + + + + + + + + + + + + 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()) { + ?> + + + + + + + + + "; + } + $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(); + ?> + +
IDNamePrep TimeDifficultyVegetarianActions
+ + +
+ + +
+ +
No records found
+ + + 1) { + echo ''; + } + ?> +
+ + diff --git a/web/rate.php b/web/rate.php new file mode 100644 index 0000000..97bfe33 --- /dev/null +++ b/web/rate.php @@ -0,0 +1,45 @@ + + + + + + Rate Recipe + + + + +
+

Rate Recipe

+ Back to List + prepare($sql); + $stmt->bind_param("i", $id); + $stmt->execute(); + $result = $stmt->get_result(); + $row = $result->fetch_assoc(); + + if ($row) { + ?> +
+ +
+ + +
+ +
+ No record found

"; + } + + $stmt->close(); + $conn->close(); + ?> +
+ + diff --git a/web/rate_action.php b/web/rate_action.php new file mode 100644 index 0000000..942b8a7 --- /dev/null +++ b/web/rate_action.php @@ -0,0 +1,19 @@ +prepare($sql); +$stmt->bind_param("ii", $rating, $id); + +if ($stmt->execute()) { + header("Location: index.php"); +} else { + echo "Error: " . $sql . "
" . $conn->error; +} + +$stmt->close(); +$conn->close(); +?> diff --git a/web/read.php b/web/read.php new file mode 100644 index 0000000..e50221c --- /dev/null +++ b/web/read.php @@ -0,0 +1,59 @@ + + + + + + Read Recipe + + + + +
+

Read Recipe

+ Back to List + prepare($sql); + $stmt->bind_param("i", $id); + $stmt->execute(); + $result = $stmt->get_result(); + $row = $result->fetch_assoc(); + + if ($row) { + ?> + + + + + + + + + + + + + + + + + + + + + +
ID
Name
Preparation Time
Difficulty
Vegetarian
+ No record found

"; + } + + $stmt->close(); + $conn->close(); + ?> +
+ + diff --git a/web/update.php b/web/update.php new file mode 100644 index 0000000..9ca3ecc --- /dev/null +++ b/web/update.php @@ -0,0 +1,60 @@ + + + + + + Update Recipe + + + + +
+

Update Recipe

+ Back to List + prepare($sql); + $stmt->bind_param("i", $id); + $stmt->execute(); + $result = $stmt->get_result(); + $row = $result->fetch_assoc(); + + if ($row) { + ?> +
+ +
+ + +
+
+ + +
+
+ + +
+
+ + +
+ +
+ No record found

"; + } + + $stmt->close(); + $conn->close(); + ?> +
+ + diff --git a/web/update_action.php b/web/update_action.php new file mode 100644 index 0000000..8e1ece5 --- /dev/null +++ b/web/update_action.php @@ -0,0 +1,22 @@ +prepare($sql); +$stmt->bind_param("sssii", $name, $prep_time, $difficulty, $vegetarian, $id); + +if ($stmt->execute()) { + header("Location: index.php"); +} else { + echo "Error: " . $sql . "
" . $conn->error; +} + +$stmt->close(); +$conn->close(); +?>