# Deploy a Composer PHP app on Yeeted

A small slug API in **PHP 8.3 managed by Composer**, with **no Dockerfile**:
Yeeted sees `composer.json`, installs the locked dependencies with
`composer install --no-dev` from the project root, and serves only `public/`
with Apache, routing every path that is not a file to `public/index.php`. The
[guestbook](guestbook-php.md) covers the other PHP path, with no
Composer manifest at all.

One real dependency, [cocur/slugify](https://github.com/cocur/slugify), does
the slugging, pinned by `composer.lock`. The app's own classes under `src/`
load through Composer's PSR-4 autoloader, so a deploy that serves a slug has
proved the install and the generated autoloader both worked.

- `GET /api/slug?text=...` returns `{"text": ..., "slug": ...}`; a missing or
  blank `text` returns 400.
- `GET /` is a form that calls the API; `GET /healthz` returns `ok`.

Tests: `php tests/test.php` (the slug rules and every route, no server
needed) · `./smoke.sh https://...` against a deployment.

## Run locally

Use PHP 8.3 or newer with mbstring, and Composer 2. From `php/slug-composer`:

```sh
composer install
php tests/test.php
php -S 127.0.0.1:8000 -t public public/index.php
```

Open `http://localhost:8000`. The router argument is needed so API paths
reach `public/index.php` in the PHP development server. `composer.json` pins the
platform to PHP 8.3, the version the deployed image runs, so a newer local
PHP cannot lock a dependency the deployment cannot install.

## Deploy and verify

Follow the [deployment walkthrough](getting-started.md). Deploy this
folder, including `composer.json`, `composer.lock`, `src/`, `yeeted.yaml` and
`public/` with its hidden `.htaccess` (some upload tools skip dotfiles),
with no database and no Dockerfile. Do not upload `vendor/`; the install runs
on Yeeted. The manifest routes to port 80, where Apache listens.

```sh
sh smoke.sh "$PREVIEW_URL"
```

The script checks the health endpoint, the page, two slugs (one with
accents, which needs the dependency's transliteration rules), that a blank
request is refused with 400, and that `vendor/`, `src/` and `composer.lock`
answer 403 or 404.

## What to change before real use

The generated image keeps Composer dependencies beside `public/`, outside the
Apache document root. The shipped `public/.htaccess` also refuses private paths
as defense in depth. Put only assets intended for browsers in `public/`.

[All examples](index.md) · [Deploy an example](getting-started.md)
