Practical Capstone Projects

Job-Ready Web App Checklist

A portfolio application is job-ready when its claims are supported by reproducible evidence. It does not need enterprise scale. It must be installable, understandable, safe under ordinary rejected input, testable at important boundaries, and honest about unfinished work.

Review the exact revision you plan to share. “It worked last week” is not evidence for the current commit.

Record Evidence, Not Check Marks

For every claim, record:

Field Meaning
Claim The behavior or property being asserted
Evidence Exact command, test name, request, or reviewed file
Result Status, relevant output, and observed side effect
Revision Commit or immutable build identifier tested
Environment PHP version, extensions, database, and operating assumptions
Owner/next action Who fixes a failure and what proves it resolved

A checked box with no command or observation is a reminder, not proof. Do not publish secrets, session cookies, private logs, real user data, internal hostnames, or full environment files as evidence.

Classify Findings By Release Impact

Use consistent severity:

  • Blocker: credible data loss, secret exposure, authentication/authorization bypass, remote code execution, stored XSS, destructive setup, or an app that cannot start from documented instructions.
  • Major: a required workflow is broken, migrations are not repeatable, tests or production build fail, rejected input causes uncontrolled errors, or rollback/readiness evidence is absent.
  • Minor: limited usability, accessibility, documentation, or maintainability defect that does not invalidate the demonstrated workflow.
  • Note: a deliberate tradeoff or future improvement with no current release failure.

No Blocker or Major finding may be relabeled as “portfolio only.” Internet-facing demonstration applications still receive hostile traffic.

A small PHP model can make the decision deterministic:

PHP example
<?php

declare(strict_types=1);

enum ReviewSeverity: string
{
    case Blocker = 'blocker';
    case Major = 'major';
    case Minor = 'minor';
    case Note = 'note';
}

final readonly class ReviewFinding
{
    public function __construct(
        public ReviewSeverity $severity,
        public string $summary,
        public string $evidence,
        public string $nextAction,
    ) {
        if ($summary === '' || $evidence === '' || $nextAction === '') {
            throw new InvalidArgumentException('Review findings require complete evidence.');
        }
    }
}

final class ReadinessReport
{
    private array $findings = [];

    public function add(ReviewFinding $finding): void
    {
        $this->findings[] = $finding;
    }

    public function decision(): string
    {
        foreach ($this->findings as $finding) {
            if (in_array($finding->severity, [ReviewSeverity::Blocker, ReviewSeverity::Major], true)) {
                return 'not-ready';
            }
        }

        return $this->findings === [] ? 'ready' : 'ready-with-notes';
    }

    public function findings(): array
    {
        return $this->findings;
    }
}

The model prioritizes review results; it does not discover vulnerabilities automatically. Human review and behavior tests supply the evidence.

Prove Reproducible Setup

Use a clean temporary checkout or CI workspace, not the directory that already contains your database, vendor tree, or forgotten environment state. Evidence includes:

php --version
php -m
composer validate --strict
composer install --no-interaction
php bin/verify-environment.php
php bin/migrate.php
php bin/migrate.php
composer check

The README states the supported PHP version, required extensions, dependency installation, environment loading mechanism, writable directories, migration and seed commands, local server command, test/static-analysis/format commands, CLI usage, and how to create non-production demo credentials. The second migration run must be a no-op.

Confirm that the app fails clearly when required configuration is missing. Production defaults must not enable debug output or insecure session cookies. Review tracked files for .env, database files, logs, uploads, caches, coverage, private keys, access tokens, and generated runtime state.

Review PHP And Architecture Boundaries

A reviewer should trace one public read and one protected write without guessing:

public/index.php -> Request -> Router -> ProductController
-> ProductValidator/ProductStore -> TemplateRenderer -> Response

Check that:

  • every PHP file follows the project's strict-types policy;
  • Composer autoloading and namespaces match the repository layout;
  • controllers do not create PDO connections or include request-chosen files;
  • repositories own prepared SQL and explicit mapping;
  • validation rejects non-scalar and boundary values without warnings;
  • money remains integer cents through validation and persistence;
  • templates receive explicit data and restore output buffers after failure;
  • unexpected exceptions become generic outer-boundary responses and controlled logs;
  • CLI commands keep standard output, standard error, and exit status distinct;
  • dead code, debug dumps, broad suppression, and unexplained TODOs are absent from the demonstrated path.

Run syntax, tests, static analysis, and coding-standard tools at the committed configuration. Record skipped tests and ignored static-analysis findings as findings, not as a clean pass.

Exercise Security And Data Integrity

Test controls through requests, not only by reading helper functions:

  • unknown and wrong-password logins take the same public path and both consume throttle capacity;
  • successful login regenerates the session ID and password hashes are rehashed when needed;
  • role revocation takes effect on the next protected request;
  • logout is POST-only, checks CSRF, destroys server state, and expires the cookie;
  • every browser write enforces method, current authorization, CSRF, validation, and persistence in that order;
  • another user cannot read or change an object by replacing its numeric ID;
  • malicious HTML renders as text in every relevant text and attribute context;
  • SQL values are bound and pagination is bounded;
  • JSON rejects wrong media type, oversized/scalar/malformed bodies, and never leaks exception details;
  • migrations roll back failed units, detect checksum changes, and run against backups before risky releases;
  • multi-write invariants use a transaction, while filesystem/database compensation is tested explicitly.

Seed data contains no real personal information or reused password. Public demo credentials have minimal permissions and a documented reset path. Uploaded demonstration files, if supported, are isolated, size-limited, authorized on download, and removable.

Review User Experience And Accessibility

The main workflow must work without a reviewer learning hidden URLs. Check desktop and narrow layouts, keyboard navigation, visible focus, associated labels, error summaries and field messages, status announcements, logical headings, useful page titles, color contrast, and content that remains readable at zoom.

Rejected submissions preserve safe values and explain recovery. Empty, loading, success, forbidden, not-found, and unexpected-error states are intentional. Destructive actions state what will happen and do not use GET. A browser back/refresh cycle after POST does not duplicate a write.

Accessibility evidence is a combination of automated checks and keyboard/screen-reader inspection. An automated score alone is not a pass.

Prove Operations And Recovery

Deployment evidence names the immutable revision, configuration changes, backup, migration output, health/readiness request, smoke tests, and log inspection. Verify both a public read and a protected write in the target environment without using production user data.

Rollback notes identify:

  • the trigger and decision owner;
  • the previous application revision;
  • whether the new schema remains backward compatible;
  • who restores data and from which verified backup when it does not;
  • how sessions, queued work, caches, or uploaded files affect recovery.

A health response exposes no versions, paths, DSNs, or exception messages. Logs are useful enough to connect a generic public error to a protected diagnostic, but they omit passwords, tokens, cookies, secrets, and unnecessary request bodies.

Present The Project Honestly

The README opens with the actual product and supported workflow, followed by setup, verification, architecture, security decisions, deployment, and known limitations. Screenshots show real application states, including one useful narrow viewport, rather than replacing runnable evidence.

Be precise about tools: “PHPStan configured at this level with these ignored findings” is stronger than “enterprise-grade static analysis.” Explain one tradeoff using decision, benefit, cost, current evidence, and reconsideration trigger. Do not add queues, containers, microservices, or cloud services solely to create résumé keywords.

Make The Release Decision

A review ends with one of three decisions:

  • ready: no open findings;
  • ready-with-notes: only Minor or Note findings, each recorded with ownership;
  • not-ready: at least one Blocker or Major finding.

Fix the highest-severity finding, rerun the smallest relevant evidence, then rerun the full clean-checkout gate before changing the decision. Preserve the review report with the tested revision so claims remain auditable.

Practice

Practice: Review A Portfolio App

Review one exact revision of the PHP capstone and make an evidence-based release decision.

Requirements

  • Record claim, evidence, result, revision, environment, and next action.
  • Classify every finding as Blocker, Major, Minor, or Note using the lesson definitions.
  • Reproduce setup, configuration failure, two migration runs, and all automated checks in a clean workspace.
  • Trace one public read and one protected write through the real architecture.
  • Exercise authentication, authorization, CSRF, validation, escaping, SQL, JSON, CLI, and exception boundaries.
  • Verify database effects, not only response text.
  • Review tracked files and evidence artifacts for secrets and private data.
  • Test keyboard, focus, labels, errors, status messages, headings, zoom, and narrow layouts.
  • Verify deployment, health, logs, backup, rollback, and data-restoration ownership.
  • State one tradeoff with its benefit, cost, evidence, and reconsideration trigger.
  • Do not add technology merely to improve the tool list.

Produce a review report, evidence table, ordered findings, ready/ready-with-notes/not-ready decision, and remediation proof for the highest-severity finding. After the targeted proof passes, rerun the full clean-checkout gate and record the new decision.

Show solution

A useful solution is a completed report tied to an exact revision. This example shows the required specificity.

Evidence Summary

Revision: 4f12abc
Environment: PHP 8.5.x, pdo_sqlite, fileinfo, clean Linux workspace
Setup: composer install; verify-environment; migrate twice; composer check
Architecture: real GET /products and authenticated POST /products traced
Security: login/throttle/role/CSRF/escaping/ID checks exercised with database assertions
Operations: backup, migration, readiness, smoke, log, and rollback drill recorded

Initial Findings

BLOCKER  .env containing a real mail token is tracked.
Evidence: tracked-file review at revision 4f12abc.
Action: revoke token, remove file from the current tree and relevant history,
        add safe example/configuration, document incident ownership.

MAJOR    README setup omits how .env values are loaded.
Evidence: clean checkout fails ApplicationConfig::fromEnvironment().
Action: document and implement the reviewed environment-loading mechanism,
        then repeat clean setup.

MINOR    Validation summary does not receive focus after rejected POST.
Evidence: keyboard walkthrough of product-create error path.
Action: move focus to the summary and verify with keyboard and screen reader.

NOTE     SQLite limits concurrent writes but matches the single-instance demo.
Trigger: measured lock waits or multi-instance deployment requirement.

The initial ReadinessReport::decision() is not-ready because Blocker and Major findings remain.

Remediation Evidence

The secret response is not merely deleting .env from the latest commit. Revoke the credential first, review exposure, remove sensitive history according to repository-host procedures, add .env to ignore rules, commit .env.example with placeholders, and verify the application uses a replacement secret supplied outside Git.

Repeat evidence:

composer install --no-interaction
php bin/verify-environment.php
php bin/migrate.php
php bin/migrate.php
composer check

Then repeat the tracked-file review and the affected deployment smoke tests. Record the new immutable revision; do not overwrite the original report. After the environment-loading Major is also resolved, rerun the complete clean-checkout gate.

If only the accessibility Minor and SQLite Note remain, the new decision is ready-with-notes, with both findings assigned and visible. It becomes ready only after every finding is closed and its evidence rerun.

Interview Explanation

A concise explanation names the product, follows one request through the code, describes one rejected path and its test, explains the SQLite tradeoff with evidence, and states the next improvement. It does not claim scale, security certification, or production experience that the repository cannot demonstrate.