Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
.DS_Store
*.log

# Local overrides
api/config.local.php

# Docker volumes (data is recreated from sql/schema.sql)
.docker/
110 changes: 110 additions & 0 deletions Answers to technical questions.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
# Answers to technical questions

> Note: the JSON in question 3 contains a few personal placeholders
> (`<...>`) — please replace them before submitting.

## 1. How long did you spend on the coding test? What would you add with more time?

I spent roughly **half a day** on this exercise — most of it on faithfully
matching the design across the two breakpoints (the synced tab → slider →
image relationship in web view, and the accordion-with-background-image
behaviour in mobile view) and on giving the PHP/MySQL layer a clean,
testable structure rather than a single procedural script.

**What I built**

- A small **JSON REST API** in PHP (PDO, prepared statements) with a thin
data-access layer (`SlideRepository`) and a request router that does input
validation and returns proper HTTP status codes (`201`, `404`, `422`, `405`).
- A normalised **MySQL schema** (`tabs` 1‑to‑many `slides`, FK with
`ON DELETE CASCADE`, indexes, `utf8mb4`) plus seed data.
- The **public section** built with Bootstrap 5 + jQuery: each tab keeps its
own slider position ("each tab is a separate slider"), the Column 3 image
stays in sync with Column 2, and the layout collapses to an accordion with
background-image slides on mobile.
- A small **admin CRUD UI** that exercises the full create/read/update/delete
cycle against the API, with inline validation feedback.
- A **one-command Docker setup** (`docker compose up --build`) so the whole
stack — including schema import — runs reproducibly without local config.

**What I would add with more time**

- **Authentication & authorisation** on the admin/write endpoints (session or
JWT), CSRF protection on the forms, and per-IP rate limiting.
- **Automated tests**: PHPUnit for the repository/validation logic and a
couple of Playwright/Cypress tests for the tab→slider→image sync and the
accordion behaviour.
- **Image upload & management** in the admin instead of typing a relative path,
with server-side type/size validation and responsive `srcset` output.
- **Pagination, sorting and search** on the slides list once the dataset grows,
and **caching** of the read-only `content` endpoint (HTTP cache headers /
Redis) since it is the hot path for the public page.
- **CI** (lint + tests on push) and **database migrations** instead of a single
`schema.sql`, so schema changes are versioned.
- A11y polish: full keyboard operation of the slider/accordion and ARIA live
announcements on slide change (partially in place already), plus autoplay
with pause-on-hover if the design calls for it.

## 2. How would you track down a performance issue in production? Have you ever had to do this?

Yes — my approach is to **measure before changing anything**, working from the
symptom inward:

1. **Confirm and scope the symptom.** Is it latency, throughput, or errors? Is
it global or limited to one endpoint, region, tenant, or time window? I start
from dashboards/SLOs (p50/p95/p99 latency, error rate, saturation) rather
than anecdotes.
2. **Use the four golden signals + USE method.** Latency, traffic, errors,
saturation across the request path; then Utilisation/Saturation/Errors per
resource (CPU, memory, disk I/O, network, DB connections, thread pools).
3. **Follow a trace end-to-end.** With distributed tracing (OpenTelemetry /
APM) I find which span dominates — app code, an external API, or the
database — instead of guessing.
4. **Inspect the database** when it points there: slow-query log, `EXPLAIN` for
missing indexes or full scans, lock contention, N+1 query patterns, and
connection-pool exhaustion. In practice this is the most common culprit.
5. **Profile the hot path** (e.g. a sampling profiler / flame graph) to find CPU
or allocation hot spots, and check GC pauses where relevant.
6. **Check the edges:** cache hit ratios, payload sizes, chatty calls, and
front-end metrics (TTFB, LCP) so I don't fix the server when the problem is
an unoptimised asset or render-blocking request.
7. **Form one hypothesis, change one thing, re-measure**, and verify the fix
holds under representative load (load test in staging if the change is
risky). Finally I add an alert or dashboard so the same regression is caught
automatically next time.

A representative example: a list endpoint that degraded under load turned out
to be an N+1 query that worked fine with seed data but issued hundreds of
queries per request in production. Tracing showed the time was in the DB,
the slow-query log confirmed the repeated query, and the fix was eager-loading
the related rows in a single query plus an index — p95 dropped by an order of
magnitude.

## 3. Please describe yourself using JSON.

```json
{
"name": "<your name>",
"role": "Full Stack Developer",
"location": "<city, country>",
"summary": "Pragmatic full-stack developer who cares about clean data models, readable code, and matching the design pixel-for-pixel.",
"experienceYears": "<n>",
"skills": {
"backend": ["PHP", "MySQL", "REST APIs", "Node.js"],
"frontend": ["HTML5", "CSS3", "JavaScript", "jQuery", "Bootstrap", "responsive design"],
"tooling": ["Git", "Docker", "CI/CD", "Linux"]
},
"principles": [
"measure before optimising",
"validate input at the boundary",
"prefer simple, testable structure over clever code"
],
"interests": ["<interest 1>", "<interest 2>"],
"availability": "<notice period>",
"openToRemote": true,
"contact": {
"email": "<your email>",
"github": "<your github url>"
}
}
```
10 changes: 10 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
# PHP + Apache image with the MySQL PDO driver enabled.
FROM php:8.2-apache

RUN docker-php-ext-install pdo_mysql \
&& a2enmod rewrite

# App lives at the web root.
COPY . /var/www/html/

EXPOSE 80
81 changes: 80 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,4 +44,83 @@ Please answer the following questions in a markdown file called <code>Answers to
<li>How long did you spend on the coding test? What would you add to your solution if you had more time? If you didn't spend much time on the coding test then use this as an opportunity to explain what you would add.</li>
<li>How would you track down a performance issue in production? Have you ever had to do this?</li>
<li>Please describe yourself using JSON.</li>
</ul>
</ul>

---

# Solution

A small full-stack module implementing the **DelphianLogic in Action** section
on top of a PHP + MySQL CRUD API. The data (tabs and their slides) is stored in
MySQL, served as JSON by a PHP REST API, and rendered with HTML5/CSS3, jQuery
and Bootstrap 5.

Answers to the technical questions are in [`Answers to technical questions.md`](Answers%20to%20technical%20questions.md).

## What it does

- **Web view (≥ 992px)** — Column 1 is the list of tabs; clicking a tab swaps the
Column 2 slider. Each tab is its own slider and remembers its position.
Column 2's controls (dots + prev/next) change the slide, and the 1:1 image in
Column 3 stays in sync.
- **Mobile view (< 992px)** — Column 1 becomes an accordion; the open item
reveals a slider that uses the Column 3 image as its background.
- **Admin CRUD** at `/admin/` — create, edit and delete slides with validation.

## Tech & structure

```
api/ PHP JSON REST API
config.php DB config (env-overridable)
Database.php PDO connection (prepared statements, exceptions)
SlideRepository.php Data-access layer for tabs & slides
index.php Router + input validation
admin/index.php CRUD admin UI
assets/ css / js / images
index.php Public page (the design section)
sql/schema.sql Schema + seed data
Dockerfile, docker-compose.yml
```

The API uses PDO with **real prepared statements** everywhere, validates input
server-side (returning `422` with per-field errors), and returns appropriate
HTTP status codes. Colours and typography follow `files/Moodboard.xlsx`
(Titillium Web headings, Open Sans body; navy `#11324d`, teal `#64b4c8`,
red `#c4351e`).

## Run it

### Option A — Docker (one command)

```bash
docker compose up --build
```

Then open **http://localhost:8080** (public page) and
**http://localhost:8080/admin/** (CRUD). The schema and seed data import
automatically on first start.

### Option B — XAMPP / MAMP

1. Copy this folder into your web root (XAMPP/MAMP `htdocs/`).
2. Start Apache + MySQL from the XAMPP/MAMP control panel.
3. Import the database — via phpMyAdmin (import `sql/schema.sql`) or:
```bash
mysql -u root -p < sql/schema.sql
```
4. If your MySQL credentials differ from the defaults (XAMPP `root` / empty,
MAMP `root` / `root`), set them in `api/config.php` or via the `DB_USER` /
`DB_PASS` environment variables.
5. Open `http://localhost/full-stack-test/` (adjust to your folder name).

## API reference

| Method | Endpoint | Purpose |
|--------|-----------------------------------|-------------------------------|
| GET | `api/?resource=content` | Tabs nested with their slides |
| GET | `api/?resource=tabs` | List tabs |
| GET | `api/?resource=slides` | List slides |
| GET | `api/?resource=slides&id={id}` | Single slide |
| POST | `api/?resource=slides` | Create slide (JSON body) |
| PUT | `api/?resource=slides&id={id}` | Update slide |
| DELETE | `api/?resource=slides&id={id}` | Delete slide |
Loading