8. Step-by-Step: Build a PHP + MySQL Art-Selling Website
Goal: A simple but production-minded site for a local artist to sell artwork online:
- Public gallery + artwork detail pages
- Cart + checkout (via Stripe Checkout)
- Admin dashboard (login) to manage artworks & orders
- MySQL database
- Secure basics (sessions, CSRF, input validation, prepared statements)
You'll "vibe code" by never manually writing full features—you'll instead direct Cursor to generate, refactor, and fix, step by step.
0 Prep: What you need locally
Minimum stack
- PHP 8.1+
- MySQL 8+ (or MariaDB)
- A local web server (Apache/Nginx)
- Composer (for Stripe SDK and dotenv)
Folder for your project
Create: local-artist-artshop/
1 The Vibe Coding "Rules of Engagement"
Before writing anything, set the operating constraints in Cursor.
Cursor prompt (paste into chat in Cursor)
You are a senior PHP engineer. We will build a small e-commerce website for a local artist.
Constraints: PHP 8+, MySQL, server-rendered pages (no heavy frontend frameworks), minimal JS.
Must be secure by default: prepared statements, CSRF for forms, input validation, session hardening, and an admin login.
Use a clean structure: public/ as web root, app/ for logic, templates/ for views.
Use PDO. No ORM.
Use Stripe Checkout for payments (do not store card data).
Generate code incrementally, with small diffs and clear file paths.
Always explain what files to create/change and why.
Why this matters
This "anchors" your vibe so Cursor doesn't generate messy spaghetti code.
2 Scaffold the project structure (Cursor generates it)
Cursor prompt
"Create a project folder structure for this site with: routing, controllers, templates, database connection, basic config management via .env, and a minimal CSS file. Provide the tree and create empty files with boilerplate."
Expected structure
local-artist-artshop/
public/
index.php
assets/
styles.css
app/
bootstrap.php
config.php
db.php
router.php
controllers/
HomeController.php
ArtworkController.php
CartController.php
CheckoutController.php
AdminController.php
middleware/
Auth.php
Csrf.php
services/
ArtworkService.php
OrderService.php
StripeService.php
helpers/
security.php
validators.php
templates/
layout.php
home.php
artwork_list.php
artwork_detail.php
cart.php
checkout_success.php
checkout_cancel.php
admin_login.php
admin_dashboard.php
admin_artwork_form.php
storage/
uploads/
vendor/ (composer)
.env
.env.example
composer.json
Vibe coding tip
Tell Cursor to keep public/ as the only web-accessible directory.
3 Create the MySQL schema (Cursor writes SQL)
You want tables for: artworks, orders, order_items, users (admin), inventory (optional).
Cursor prompt
"Design a MySQL schema for an art e-commerce site. Requirements: artworks with title, description, price, stock, dimensions, medium, images, is_published; orders with status, totals, buyer info, Stripe session id; order_items referencing artworks; users for admin login with password_hash. Provide SQL migrations."
You should end up with something like
CREATE TABLE users (
id BIGINT UNSIGNED PRIMARY KEY AUTO_INCREMENT,
email VARCHAR(190) NOT NULL UNIQUE,
password_hash VARCHAR(255) NOT NULL,
role VARCHAR(50) NOT NULL DEFAULT 'admin',
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE artworks (
id BIGINT UNSIGNED PRIMARY KEY AUTO_INCREMENT,
slug VARCHAR(190) NOT NULL UNIQUE,
title VARCHAR(255) NOT NULL,
description TEXT NULL,
medium VARCHAR(120) NULL,
dimensions VARCHAR(120) NULL,
price_cents INT NOT NULL,
currency CHAR(3) NOT NULL DEFAULT 'USD',
stock INT NOT NULL DEFAULT 0,
image_path VARCHAR(255) NULL,
is_published TINYINT(1) NOT NULL DEFAULT 0,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
);
CREATE TABLE orders (
id BIGINT UNSIGNED PRIMARY KEY AUTO_INCREMENT,
order_number VARCHAR(30) NOT NULL UNIQUE,
status VARCHAR(40) NOT NULL DEFAULT 'pending',
buyer_name VARCHAR(255) NULL,
buyer_email VARCHAR(190) NULL,
total_cents INT NOT NULL DEFAULT 0,
currency CHAR(3) NOT NULL DEFAULT 'USD',
stripe_session_id VARCHAR(255) NULL UNIQUE,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE order_items (
id BIGINT UNSIGNED PRIMARY KEY AUTO_INCREMENT,
order_id BIGINT UNSIGNED NOT NULL,
artwork_id BIGINT UNSIGNED NOT NULL,
quantity INT NOT NULL DEFAULT 1,
unit_price_cents INT NOT NULL,
FOREIGN KEY (order_id) REFERENCES orders(id) ON DELETE CASCADE,
FOREIGN KEY (artwork_id) REFERENCES artworks(id) ON DELETE RESTRICT
);
4 Configure environment + DB connection (Cursor writes .env + PDO)
.env.example
APP_ENV=local
APP_URL=http://localhost
DB_HOST=127.0.0.1
DB_NAME=artshop
DB_USER=root
DB_PASS=
STRIPE_SECRET_KEY=sk_test_xxx
STRIPE_WEBHOOK_SECRET=whsec_xxx
Cursor prompt
"Implement app/db.php using PDO with proper error mode, charset utf8mb4, and safe connection handling. Also add app/config.php and app/bootstrap.php to load env values and start a secure PHP session."
Vibe coding checkpoints
session.cookie_httponly=1
session.cookie_secure=1 in prod
session.use_strict_mode=1
- Regenerate session id after login
5 Routing (Cursor creates a tiny router)
Cursor prompt
"Create a minimal router that maps routes to controller methods. Routes needed:"
GET / (home)
GET /art (list)
GET /art/{slug} (detail)
POST /cart/add
GET /cart
POST /cart/remove
POST /checkout/create-session
GET /checkout/success
GET /checkout/cancel
GET /admin/login
POST /admin/login
GET /admin
GET /admin/art/new
POST /admin/art/create
GET /admin/art/edit/{id}
POST /admin/art/update/{id}
POST /admin/art/delete/{id}
"Use templates and a shared layout."
6 Public pages: gallery + artwork details
Cursor prompt
"Implement the Home and Artwork pages with templates. Home shows featured published artworks (limit 6). /art shows all published artworks with pagination. /art/{slug} shows details, image, and 'Add to cart' if stock > 0. Use a clean HTML layout and minimal CSS."
Vibe coding tip
Insist on server-rendered HTML to keep it simple and reliable.
7 Cart in session (no DB yet)
Cursor prompt
"Implement CartController storing cart items in PHP session. Cart key: artwork_id, quantity. Validate stock when adding. Provide cart template showing subtotal and 'Checkout' button. Add CSRF tokens to forms for add/remove actions."
Definition of "done"
- Add to cart works
- Remove works
- Quantity limits enforced (0..stock)
- CSRF required on POST
8 Checkout with Stripe Checkout (recommended)
You should not implement card handling yourself. Stripe Checkout is the safe, standard path.
Composer
In terminal:
composer require stripe/stripe-php vlucas/phpdotenv
Cursor prompt
"Implement Stripe Checkout: CheckoutController creates a Stripe Checkout Session from cart items. Use price_data with unit_amount (cents), product_data name. Redirect to session URL. On success page: show 'Thanks' and an order number. Create an order record in MySQL when checkout session is created (pending), then mark paid via webhook. Provide StripeService.php and the needed endpoints."
Webhooks
Cursor prompt: "Add a webhook endpoint POST /stripe/webhook under public/stripe_webhook.php. Verify signature with STRIPE_WEBHOOK_SECRET. On checkout.session.completed: mark order as paid; decrement stock for each order item (transaction); store buyer email/name if available. Use transactions and prevent double-processing (idempotency by stripe_session_id)."
Vibe coding friendly
This is very vibe-coding-friendly because you can ask Cursor to focus on correctness + idempotency.
9 Admin area (login + artwork CRUD)
Cursor prompt
"Build an admin login with email/password. Use password_hash/password_verify. Protect all /admin routes with middleware. Admin dashboard: list artworks, quick actions to publish/unpublish, edit, delete. Add artwork create/edit forms with image upload to storage/uploads (validate file type/size). Store image path in DB. Add basic server-side validation messages in templates."
Minimum security asks
- Validate uploaded images: MIME + extension + size
- Randomize file names
- Never allow PHP uploads into web root
- Escape output in templates
10 Make it "real" (content, SEO, and trust)
Cursor prompt
"Add SEO basics: page titles, meta descriptions, Open Graph tags. Add About page for the artist, Contact section, and a small FAQ about shipping/returns. Add a footer with social links. Keep it clean and professional."
Add
- Shipping note (even if manual)
- Returns policy
- Privacy note (basic)
11 Hardening pass (this is where vibe coding shines)
Cursor prompt
"Do a security and quality hardening pass: Ensure all DB access uses prepared statements. Ensure all POST forms have CSRF protection. Escape all dynamic output in templates. Add basic rate limiting to admin login (simple session-based). Add centralized error handling and a safe 500 page. Provide a checklist of what you changed."
12 Deployment-ready steps (cPanel/VPS friendly)
Cursor prompt
"Provide deployment instructions for a standard PHP hosting environment: Set public/ as document root. Configure .env for production. Run DB migrations. Ensure storage/uploads writable. Configure HTTPS. Set secure session cookie settings in production."