From 6b3fd8bd3083602dd49af404db50397ff1c6ec30 Mon Sep 17 00:00:00 2001 From: roadynet <163515962+roadynet@users.noreply.github.com> Date: Mon, 15 Jun 2026 09:04:57 +0200 Subject: [PATCH 1/4] Harden public showcase safety checks --- .gitattributes | 3 + .github/workflows/portfolio-audit.yml | 5 +- .gitignore | 6 + CHANGELOG.md | 6 + README.md | 3 + docs/evidence/README.md | 1 + .../public-live-readiness-2026-06-15.md | 51 +++++ docs/quality-report.md | 15 ++ tests/showcase_audit.py | 198 ++++++++++++++++++ 9 files changed, 287 insertions(+), 1 deletion(-) create mode 100644 .gitattributes create mode 100644 docs/evidence/public-live-readiness-2026-06-15.md create mode 100644 tests/showcase_audit.py diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..80928f1 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,3 @@ +* text=auto eol=lf + +*.png binary diff --git a/.github/workflows/portfolio-audit.yml b/.github/workflows/portfolio-audit.yml index ba03ea7..91f3bc7 100644 --- a/.github/workflows/portfolio-audit.yml +++ b/.github/workflows/portfolio-audit.yml @@ -29,11 +29,14 @@ jobs: set -euo pipefail while IFS= read -r file; do php -l "$file" - done < <(find examples -name '*.php' -type f | sort) + done < <(find examples tests -name '*.php' -type f | sort) - name: Run public PHPUnit examples run: phpunit + - name: Run public showcase safety audit + run: python tests/showcase_audit.py + - name: Check local Markdown links shell: python run: | diff --git a/.gitignore b/.gitignore index 2ad7306..6059378 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,12 @@ /vendor/ /.phpunit.cache/ +/__pycache__/ +**/__pycache__/ /.env /.env.* !/.env.example !/screenshots/.gitkeep +/config/secrets/ +/private-config/ +/public/uploads/ +/var/ diff --git a/CHANGELOG.md b/CHANGELOG.md index bdbb0ed..5be8b06 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## 2026-06-15 + +- Added a repeatable public showcase safety audit for env-file policy, obvious secret patterns, private-code path leakage, local links, screenshots, wording consistency, and PHPUnit XML parsing. +- Added public live readiness evidence for the live demo's public routes and security headers. +- Extended the Portfolio Audit workflow to run the showcase safety audit and lint PHP files in both `examples/` and `tests/`. + ## 2026-05-26 - Added interview demo script for SkillBuilder and the Shopware bridge. diff --git a/README.md b/README.md index 221fe2c..58799dc 100644 --- a/README.md +++ b/README.md @@ -187,6 +187,7 @@ Public showcase checks: ```bash phpunit +python tests/showcase_audit.py ``` Current public example test result: @@ -210,6 +211,7 @@ OK Additional checks performed: +- public showcase safety audit for env-file policy, obvious secret patterns, private-code path leakage, local links, screenshots, and wording consistency - PHP syntax check across 180 files - Twig lint across 38 templates - YAML lint across 18 config files @@ -219,6 +221,7 @@ Additional checks performed: - live smoke checks for public and protected routes - PHPStan audit setup added in the private codebase; baseline reduced from 56 findings to 17 Doctrine generated-ID findings - Evidence notes added for the PHPStan baseline reduction and private codebase verification limits +- Public live readiness evidence added for 2026-06-15 ## Portfolio Positioning diff --git a/docs/evidence/README.md b/docs/evidence/README.md index 0c1a8e5..574effe 100644 --- a/docs/evidence/README.md +++ b/docs/evidence/README.md @@ -8,6 +8,7 @@ The private repository is not published here. Evidence files summarize relevant - [PHPStan baseline reduction](phpstan-baseline-reduction-2026-06-05.md) - [Private codebase verification notes](private-codebase-verification-2026-06-05.md) +- [Public live readiness evidence](public-live-readiness-2026-06-15.md) ## Interpretation diff --git a/docs/evidence/public-live-readiness-2026-06-15.md b/docs/evidence/public-live-readiness-2026-06-15.md new file mode 100644 index 0000000..2f12b33 --- /dev/null +++ b/docs/evidence/public-live-readiness-2026-06-15.md @@ -0,0 +1,51 @@ +# Public Live Readiness Evidence - 2026-06-15 + +## Scope + +This note records portfolio-safe checks performed for the public SkillBuilder showcase and the public surface of the live demo. + +The private application source code, credentials, database configuration, and user data are not included in this repository. + +## Public Showcase Checks + +Repository target: + +```text +roadynet/skillbuilder-showcase +``` + +Local checks performed: + +```text +PHP syntax check for examples and tests: OK +PHPUnit 11.5.55 on PHP 8.4.21: OK, 4 tests, 7 assertions +Local Markdown links: OK +Public wording consistency: OK +Environment file policy: OK +Obvious secret patterns: OK +Screenshot existence and PNG signatures: OK +phpunit.xml XML parsing: OK +``` + +The showcase now includes `tests/showcase_audit.py`, which keeps these public-safety checks repeatable in GitHub Actions. + +## Live Demo Public Smoke Check + +Public routes checked without publishing credentials: + +| Route | Result | Security headers | +| --- | --- | --- | +| `/health` | 200 | CSP and nosniff present | +| `/login` | 200 | CSP and nosniff present | +| `/register` | 200 | CSP and nosniff present | +| `/impressum` | 200 | CSP and nosniff present | +| `/dsgvo` | 200 | CSP and nosniff present | + +The canonical public privacy route for this project is `/dsgvo`. + +## Notes + +- Demo credentials remain unpublished. +- `.env.example` contains placeholders only. +- The public showcase intentionally excludes private Symfony source directories such as `src/`, `config/`, `templates/`, `migrations/`, `var/`, and private config files. +- The live application has additional protected-route checks, but those are summarized only at a high level in public documentation. diff --git a/docs/quality-report.md b/docs/quality-report.md index 503edf9..e9e3cfb 100644 --- a/docs/quality-report.md +++ b/docs/quality-report.md @@ -14,6 +14,19 @@ OK The public tests cover representative examples for learning scheduling and section-code parsing. +Public showcase safety audit: + +```text +python tests/showcase_audit.py + +private application paths are not tracked +environment file policy is valid +obvious secret patterns are absent +local Markdown links and screenshots are valid +public wording is consistent +phpunit.xml is valid XML +``` + Current private test suite: ```text @@ -64,12 +77,14 @@ Performed checks: - Symfony container lint - Doctrine mapping validation - live smoke checks against production routes +- public showcase safety audit for secrets, private-code leaks, local links, screenshots, and wording consistency Detailed evidence: - [Evidence index](evidence/README.md) - [PHPStan baseline reduction evidence](evidence/phpstan-baseline-reduction-2026-06-05.md) - [Private codebase verification notes](evidence/private-codebase-verification-2026-06-05.md) +- [Public live readiness evidence](evidence/public-live-readiness-2026-06-15.md) ## Important Finding Fixed diff --git a/tests/showcase_audit.py b/tests/showcase_audit.py new file mode 100644 index 0000000..6951efe --- /dev/null +++ b/tests/showcase_audit.py @@ -0,0 +1,198 @@ +#!/usr/bin/env python3 +"""Portfolio-safety checks for the public SkillBuilder showcase.""" + +from __future__ import annotations + +import re +import subprocess +import sys +import xml.etree.ElementTree as ET +from pathlib import Path +from urllib.parse import unquote + + +ROOT = Path(__file__).resolve().parents[1] +TEXT_SUFFIXES = {".md", ".php", ".svg", ".txt", ".yml", ".yaml", ".xml"} +ALLOWED_ENV_FILES = {Path(".env.example")} +IGNORED_SCAN_DIRS = {".git", "vendor", ".phpunit.cache"} + + +def fail(message: str) -> None: + print(message) + sys.exit(1) + + +def tracked_files(*patterns: str) -> list[Path]: + cmd = ["git", "ls-files", *patterns] + output = subprocess.check_output(cmd, cwd=ROOT, text=True) + return [Path(line) for line in output.splitlines() if line.strip()] + + +def read_text(path: Path) -> str: + return (ROOT / path).read_text(encoding="utf-8", errors="ignore") + + +def check_no_private_project_files() -> None: + forbidden_roots = { + "config", + "migrations", + "private-config", + "src", + "templates", + "var", + } + failures: list[str] = [] + + for path in tracked_files(): + normalized = path.as_posix() + first_part = path.parts[0] if path.parts else "" + if first_part in forbidden_roots or normalized.startswith("public/uploads/"): + failures.append(f"{path}: private application path must not be tracked") + + if failures: + fail("\n".join(failures)) + + print("OK private application paths are not tracked") + + +def check_env_file_policy() -> None: + failures: list[str] = [] + + for path in tracked_files(): + if path.name.startswith(".env") and path not in ALLOWED_ENV_FILES: + failures.append(f"{path}: only .env.example is allowed") + + for path in ROOT.rglob(".env*"): + relative = path.relative_to(ROOT) + if any(part in IGNORED_SCAN_DIRS for part in relative.parts): + continue + if relative not in ALLOWED_ENV_FILES: + failures.append(f"{relative}: real env files must stay outside the showcase") + + if failures: + fail("\n".join(failures)) + + print("OK environment file policy") + + +def check_secret_patterns() -> None: + patterns = [ + re.compile(r"github_pat_[A-Za-z0-9_]+"), + re.compile(r"gh[pousr]_[A-Za-z0-9_]{20,}"), + re.compile(r"sk-[A-Za-z0-9]{20,}"), + re.compile(r"-----BEGIN (?:RSA |EC |OPENSSH |DSA )?PRIVATE KEY-----"), + re.compile(r"gpt@sb\.local", re.IGNORECASE), + re.compile(r"sb\.local", re.IGNORECASE), + ] + failures: list[str] = [] + + for path in tracked_files(): + if path.suffix.lower() not in TEXT_SUFFIXES and path.name != ".env.example": + continue + text = read_text(path) + for pattern in patterns: + if pattern.search(text): + failures.append(f"{path}: possible private credential or key pattern") + + if failures: + fail("\n".join(failures)) + + print("OK obvious secret patterns") + + +def check_placeholder_env() -> None: + text = (ROOT / ".env.example").read_text(encoding="utf-8") + required_placeholders = [ + "APP_SECRET=change-me-in-server-environment", + "DATABASE_URL=mysql://db_user:db_password@localhost:3306/db_name", + "SHOPWARE_ADMIN_BASE_URL=https://example-shop.test", + "SHOPWARE_ADMIN_USERNAME=your-admin-user", + "SHOPWARE_ADMIN_PASSWORD=your-admin-password", + ] + missing = [line for line in required_placeholders if line not in text] + + if missing: + fail(".env.example is missing expected placeholder values:\n" + "\n".join(missing)) + + print("OK env placeholders are explicit examples") + + +def check_markdown_links_and_images() -> None: + pattern = re.compile(r"(?P!)?\[[^\]]*]\(([^)]+)\)") + failures: list[str] = [] + + for path in tracked_files("*.md"): + text = read_text(path) + for match in pattern.finditer(text): + target = match.group(2).strip() + if not target or target.startswith(("http://", "https://", "mailto:", "#")): + continue + + clean = unquote(target.split("#", 1)[0]) + if not clean: + continue + + resolved = (ROOT / path.parent / clean).resolve() + try: + resolved.relative_to(ROOT) + except ValueError: + failures.append(f"{path}: local link escapes repository: {target}") + continue + + if not resolved.exists(): + failures.append(f"{path}: missing local link target {target}") + continue + + if match.group("image") and resolved.suffix.lower() == ".png": + data = resolved.read_bytes() + if len(data) < 1024: + failures.append(f"{path}: screenshot {target} is unexpectedly small") + if not data.startswith(b"\x89PNG\r\n\x1a\n"): + failures.append(f"{path}: screenshot {target} is not a PNG") + + if failures: + fail("\n".join(failures)) + + print("OK local markdown links and screenshots") + + +def check_public_wording() -> None: + forbidden = [ + "Kapitel als " + "Kategorien", + "Kategorien " + "exportieren", + "Kapitel werden " + "Kategorien", + "chapters become " + "categories", + "Lessons als Produkte, " + "Kapitel", + "/datenschutz", + ] + failures: list[str] = [] + + for path in tracked_files("*.md", "*.php", "*.svg", "*.txt", "*.yml", "*.yaml"): + text = read_text(path).lower() + for phrase in forbidden: + if phrase.lower() in text: + failures.append(f"{path}: outdated or non-canonical public wording found") + + if failures: + fail("\n".join(failures)) + + print("OK public wording consistency") + + +def check_phpunit_config() -> None: + ET.parse(ROOT / "phpunit.xml") + print("OK phpunit.xml is valid XML") + + +def main() -> None: + check_no_private_project_files() + check_env_file_policy() + check_secret_patterns() + check_placeholder_env() + check_markdown_links_and_images() + check_public_wording() + check_phpunit_config() + + +if __name__ == "__main__": + main() From 9a206f5048501fb841f5763fe89774ef449e0523 Mon Sep 17 00:00:00 2001 From: roadynet <163515962+roadynet@users.noreply.github.com> Date: Mon, 15 Jun 2026 09:24:09 +0200 Subject: [PATCH 2/4] Refine showcase for senior Symfony review --- CHANGELOG.md | 2 + README.md | 5 +- docs/architecture.md | 26 +++++ docs/case-study.md | 18 +++- docs/code-walkthrough.md | 4 +- .../public-live-readiness-2026-06-15.md | 2 +- docs/interview-qa.md | 14 +++ docs/quality-report.md | 8 +- docs/senior-symfony-review.md | 97 +++++++++++++++++++ .../NextDueQuestionServiceExampleTest.php | 51 ++++++++++ 10 files changed, 219 insertions(+), 8 deletions(-) create mode 100644 docs/senior-symfony-review.md create mode 100644 examples/tests/NextDueQuestionServiceExampleTest.php diff --git a/CHANGELOG.md b/CHANGELOG.md index 5be8b06..3256148 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,8 @@ ## 2026-06-15 +- Added a Senior Symfony review guide that explains architecture boundaries, trade-offs, operational evidence, and reviewer prompts. +- Added PHPUnit coverage for the public next-due-question example. - Added a repeatable public showcase safety audit for env-file policy, obvious secret patterns, private-code path leakage, local links, screenshots, wording consistency, and PHPUnit XML parsing. - Added public live readiness evidence for the live demo's public routes and security headers. - Extended the Portfolio Audit workflow to run the showcase safety audit and lint PHP files in both `examples/` and `tests/`. diff --git a/README.md b/README.md index 58799dc..084ed4d 100644 --- a/README.md +++ b/README.md @@ -140,6 +140,7 @@ Only published SkillBuilder lessons are synchronized as Shopware products. Draft - [Portfolio audit report](docs/audit-report-2026-05-23.md) - [PHPStan audit evidence](docs/phpstan-audit-2026-06-05.md) - [Evidence index](docs/evidence/README.md) +- [Senior Symfony review guide](docs/senior-symfony-review.md) - [Demo script](docs/demo-script.md) - [Changelog](CHANGELOG.md) - [Demo checklist](docs/demo-checklist.md) @@ -193,8 +194,8 @@ python tests/showcase_audit.py Current public example test result: ```text -4 tests -7 assertions +7 tests +10 assertions OK ``` diff --git a/docs/architecture.md b/docs/architecture.md index e3cab36..9e9d8cf 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -41,6 +41,29 @@ Representative service responsibilities: - export user data with the correct request owner - log sensitive GDPR access +## Review Notes For Senior Symfony Readers + +The important design boundary is that controllers do not own learning policy. They collect HTTP input, enforce route-level concerns, and hand off to services that can be tested without a browser session or database fixture. + +The private application uses Doctrine for persistence, but the public examples intentionally keep core policies in plain PHP. That makes the business rules easy to inspect: + +- scheduling changes are expressed as state transitions +- due-question selection has deterministic ordering +- section-code parsing normalizes imported filenames before they become navigation or filtering keys +- security examples describe the authorization boundary instead of hiding it inside UI behavior + +The expected Symfony shape is: + +```text +Route / Controller / Form / CSRF +-> Security role or voter decision +-> Domain service +-> Doctrine repository or external API client +-> Twig or redirect response +``` + +This keeps production behavior explainable when debugging cache, wiring, permission, or deployment issues. + ## Shopware Demo Bridge The portfolio demo includes an admin-only integration path: @@ -81,3 +104,6 @@ The private application uses: - explicit admin-only routes - access checks before sensitive workflows - safe GDPR export ownership +- server-side external API credentials +- security headers on live public routes +- public/private repository separation for portfolio safety diff --git a/docs/case-study.md b/docs/case-study.md index e139a86..3129e2b 100644 --- a/docs/case-study.md +++ b/docs/case-study.md @@ -68,11 +68,25 @@ SkillBuilder documents ongoing hands-on PHP/Symfony work after the IHK qualifica - migration hygiene checks - PHPUnit tests and Symfony lint checks +## Senior Engineering Signals + +SkillBuilder is meant to be reviewed as a maintained business application rather than as a coding exercise. + +The stronger signals are: + +- explicit separation between HTTP controllers, domain services, persistence, and external API integration +- testable learning rules that can be verified without browser state +- idempotent Shopware synchronization based on stable product numbers +- private configuration kept outside the repository tree +- privacy workflows treated as auditable operational paths +- evidence documents that record both successful checks and environment limitations +- public showcase safety checks that prevent accidental publication of private files or credentials + ## Quality Results ```text -14 PHPUnit tests -650 assertions +Private suite: 14 PHPUnit tests, 650 assertions +Public showcase: 7 PHPUnit tests, 10 assertions PHP lint: 180 files Twig lint: 38 templates YAML lint: 18 config files diff --git a/docs/code-walkthrough.md b/docs/code-walkthrough.md index 5251972..4fa124f 100644 --- a/docs/code-walkthrough.md +++ b/docs/code-walkthrough.md @@ -47,6 +47,8 @@ The service prefers: It also shows the concept of a mistake pool: questions with previous wrong answers stay visible until the user rebuilds enough correct streak. +Tests for this example cover the reviewer-facing edge cases: section filtering, unanswered-first ordering, due-date ordering, stable id fallback, and unresolved mistake counting. + ## Security Boundary File: `examples/security/RoleCheckExample.php` @@ -58,6 +60,7 @@ The private app uses Symfony Security, route access rules, CSRF protection, and Files: - `examples/tests/LearningSchedulerExampleTest.php` +- `examples/tests/NextDueQuestionServiceExampleTest.php` - `examples/tests/SectionCodeServiceExampleTest.php` The tests show how business rules can be verified without a browser or database. This is the same testing style used for the private project's core learning behavior. @@ -65,4 +68,3 @@ The tests show how business rules can be verified without a browser or database. ## What These Examples Are Not These files are not a copy of the private production repository. They are compact, publishable examples designed for portfolio review and technical interviews. - diff --git a/docs/evidence/public-live-readiness-2026-06-15.md b/docs/evidence/public-live-readiness-2026-06-15.md index 2f12b33..a43e695 100644 --- a/docs/evidence/public-live-readiness-2026-06-15.md +++ b/docs/evidence/public-live-readiness-2026-06-15.md @@ -18,7 +18,7 @@ Local checks performed: ```text PHP syntax check for examples and tests: OK -PHPUnit 11.5.55 on PHP 8.4.21: OK, 4 tests, 7 assertions +PHPUnit 11.5.55 on PHP 8.4.21: OK, 7 tests, 10 assertions Local Markdown links: OK Public wording consistency: OK Environment file policy: OK diff --git a/docs/interview-qa.md b/docs/interview-qa.md index 3d81944..46c7cad 100644 --- a/docs/interview-qa.md +++ b/docs/interview-qa.md @@ -30,6 +30,18 @@ Lessons that are no longer published should not remain visible as active shop pr It demonstrates practical backend work: service design, data modeling, authentication and role checks, status-driven workflows, integration with an external API, safe handling of environment-specific configuration, deployment, and documentation. +## What should a senior Symfony reviewer inspect first? + +Start with the [Senior Symfony review guide](senior-symfony-review.md), then read the architecture page, code walkthrough, quality report, and service examples. The key signal is not that private production code is public. It is that the system has clear boundaries, repeatable checks, operational evidence, and examples of business rules that are testable outside HTTP controllers. + +## How are controller and service responsibilities separated? + +Controllers are treated as HTTP adapters: routing, form handling, CSRF, redirects, access checks, and response rendering. Scheduling, due-question selection, import decisions, export ownership, and external sync policy belong in services so they can be tested and debugged independently. + +## What are the most important trade-offs? + +The public showcase trades full source visibility for privacy and safety. To compensate, it includes representative service examples, tests, architecture notes, live-readiness evidence, and a public audit script that guards against accidentally publishing secrets or private application paths. + ## Why is this relevant for a Shopware role? It shows hands-on work with Shopware concepts and the Shopware Admin API: product synchronization, product visibility, category assignment, API authentication, and a demo storefront that makes the result visible. @@ -45,3 +57,5 @@ The public showcase does not contain production source code, real user data, cre - stronger product metadata mapping - dedicated end-to-end tests for login and learning flows - more reporting filters in the admin area +- more voter-level authorization examples +- async retry handling for longer-running imports or external API sync diff --git a/docs/quality-report.md b/docs/quality-report.md index e9e3cfb..64375df 100644 --- a/docs/quality-report.md +++ b/docs/quality-report.md @@ -7,8 +7,8 @@ Public showcase examples: ```text phpunit -4 tests -7 assertions +7 tests +10 assertions OK ``` @@ -86,6 +86,10 @@ Detailed evidence: - [Private codebase verification notes](evidence/private-codebase-verification-2026-06-05.md) - [Public live readiness evidence](evidence/public-live-readiness-2026-06-15.md) +Related senior review context: + +- [Senior Symfony review guide](senior-symfony-review.md) + ## Important Finding Fixed Doctrine validation found an inconsistent mapping between the lesson owner relation and the user lessons collection. The relationship was corrected by adding the inverse side to the owner relation. diff --git a/docs/senior-symfony-review.md b/docs/senior-symfony-review.md new file mode 100644 index 0000000..6d354ed --- /dev/null +++ b/docs/senior-symfony-review.md @@ -0,0 +1,97 @@ +# Senior Symfony Review Guide + +This guide is written for experienced Symfony reviewers who want to evaluate the engineering shape behind SkillBuilder without access to the private production repository. + +The public showcase is intentionally evidence-oriented: it does not publish private source code, credentials, deployment scripts, user data, or database configuration. + +## Fast Review Path + +Start here: + +1. [Architecture overview](architecture.md) +2. [Code walkthrough](code-walkthrough.md) +3. [Quality report](quality-report.md) +4. [Shopware bridge](shopware-bridge.md) +5. [Public live readiness evidence](evidence/public-live-readiness-2026-06-15.md) +6. Representative examples in [`examples/`](../examples/) + +The fastest technical read is the combination of the architecture page, the service examples, and the PHPUnit tests. + +## What A Senior Reviewer Should Look For + +SkillBuilder is positioned as a backend-driven Symfony business application, not as a frontend mockup. The relevant review questions are: + +- Are HTTP concerns kept out of domain policy? +- Are learning rules testable without a browser or database? +- Are admin-only workflows explicit and protected? +- Are external API credentials kept server-side? +- Is synchronization idempotent instead of click-and-duplicate? +- Are privacy workflows treated as sensitive operational paths? +- Is public portfolio evidence separated from private implementation details? + +## Architecture Decisions + +| Decision | Reason | Evidence | +| --- | --- | --- | +| Controllers stay thin | Keeps routing, request parsing, CSRF, redirects, and forms separate from learning policy | [Architecture overview](architecture.md), [Code walkthrough](code-walkthrough.md) | +| Learning rules live in services | Scheduling, question selection, and section parsing are easier to test and review when not mixed into controllers | [`LearningSchedulerExample.php`](../examples/learning-scheduler/LearningSchedulerExample.php), [`NextDueQuestionServiceExample.php`](../examples/question-selection/NextDueQuestionServiceExample.php) | +| Doctrine is the persistence boundary | Entities represent learning state and relations; service logic decides transitions | [Case study](case-study.md), [Quality report](quality-report.md) | +| Shopware sync is admin-triggered and idempotent | Repeated imports should update existing products and deactivate obsolete ones, not create duplicates | [Shopware bridge](shopware-bridge.md) | +| Secrets stay outside Git | Public showcase has placeholders only; production values live in environment or private config outside the repo tree | [README](../README.md), [.env.example](../.env.example) | +| Public repository has its own safety audit | The showcase should prove it is safe to publish, not merely claim it | [`tests/showcase_audit.py`](../tests/showcase_audit.py) | + +## Symfony-Specific Signals + +The private application work represented here includes: + +- Symfony controllers for user, lesson, admin, legal, GDPR, and integration routes +- Symfony Security with role-based access and explicit admin-only workflows +- Symfony Forms and validation for account, registration, learning settings, lessons, KDP/export-style configuration, and data requests +- Twig templates for learner and admin workflows +- Doctrine ORM entities, repositories, mapping validation, and migration hygiene +- service-layer business rules for learning progress, due-question selection, imports, statistics, exports, and external API synchronization +- PHPUnit tests for core learning behavior +- production smoke checks for public and authenticated routes + +The public examples are reduced, but they mirror the shape of the real implementation: small services with deterministic inputs, explicit state transitions, and tests around business rules. + +## Operational Reality + +The project also documents non-glamorous production concerns: + +- shared-hosting deployment constraints +- MariaDB/Doctrine mapping verification +- live smoke checks after cache and configuration changes +- external secrets file pattern for server-only credentials +- public/private repository split +- evidence notes for checks that are safe to publish + +This matters because many Symfony failures are operational rather than syntactic: stale containers, miswired services, environment drift, wrong cache state, or credentials leaking into the wrong layer. + +## Honest Boundaries + +This public repository is not a complete code review substitute. It deliberately omits: + +- production Symfony source code +- database schema and migrations +- credentials and `.env` files +- private deployment scripts +- user data and export payloads +- full admin implementation details + +The value of the showcase is therefore not "read every private line of code". It is: inspect the architecture story, verify the public examples, review the repeatable checks, and use the live walkthrough to discuss the real system. + +## Senior Discussion Prompts + +Good interview questions for this project: + +- How would you move more learning rules from ad hoc controller flows into services? +- Where would transaction boundaries belong in the Shopware synchronization? +- Which routes should require voters instead of simple role checks? +- How would you model retries and partial failure for external API sync? +- Which Doctrine relations should be lazy, eager, or projected into DTOs for admin statistics? +- How would you add browser-level regression tests without leaking demo credentials? +- How would you tighten CSP once inline scripts are fully removed? +- What should become asynchronous if lesson imports or Shopware sync grow? + +These are intentionally senior-level questions. They make the showcase a starting point for a technical conversation rather than a static brochure. diff --git a/examples/tests/NextDueQuestionServiceExampleTest.php b/examples/tests/NextDueQuestionServiceExampleTest.php new file mode 100644 index 0000000..4b1b9af --- /dev/null +++ b/examples/tests/NextDueQuestionServiceExampleTest.php @@ -0,0 +1,51 @@ +modify('-2 hours'), wrongCount: 1)), + new QuestionCandidate(3, '1.1'), + new QuestionCandidate(1, '2.1'), + ]; + + self::assertSame(3, $service->nextDueQuestion($questions, '1.1', $now)?->id); + } + + public function testDueQuestionsUseDueDateThenStableIdOrdering(): void + { + $service = new NextDueQuestionServiceExample(); + $now = new DateTimeImmutable('2026-06-15 09:00:00'); + + $questions = [ + new QuestionCandidate(4, '1.1', new QuestionProgress($now->modify('-30 minutes'), wrongCount: 1)), + new QuestionCandidate(2, '1.1', new QuestionProgress($now->modify('-2 hours'), wrongCount: 1)), + new QuestionCandidate(1, '1.1', new QuestionProgress($now->modify('+1 day'), wrongCount: 1)), + ]; + + self::assertSame(2, $service->nextDueQuestion($questions, '1.1', $now)?->id); + } + + public function testMistakePoolCountsOnlyDueUnresolvedMistakes(): void + { + $service = new NextDueQuestionServiceExample(); + $now = new DateTimeImmutable('2026-06-15 09:00:00'); + + $questions = [ + new QuestionCandidate(1, '1.1', new QuestionProgress($now->modify('-1 hour'), wrongCount: 1)), + new QuestionCandidate(2, '1.1', new QuestionProgress($now->modify('+1 hour'), wrongCount: 1)), + new QuestionCandidate(3, '1.1', new QuestionProgress($now->modify('-1 hour'), correctStreak: 2, wrongCount: 1)), + new QuestionCandidate(4, '1.1', new QuestionProgress($now->modify('-1 hour'))), + ]; + + self::assertSame(1, $service->countDueMistakes($questions, $now)); + } +} From 653b5eef76c6b195dc07ff2f07698a0b4ee4c708 Mon Sep 17 00:00:00 2001 From: roadynet <163515962+roadynet@users.noreply.github.com> Date: Mon, 15 Jun 2026 11:32:18 +0200 Subject: [PATCH 3/4] Add PHPStan to public showcase checks --- .github/workflows/portfolio-audit.yml | 5 +++- .gitignore | 1 + CHANGELOG.md | 1 + README.md | 2 ++ .../public-live-readiness-2026-06-15.md | 1 + docs/quality-report.md | 9 +++++++ docs/senior-symfony-review.md | 2 ++ phpstan.neon | 6 +++++ tests/phpstan/phpunit-bootstrap.php | 26 +++++++++++++++++++ 9 files changed, 52 insertions(+), 1 deletion(-) create mode 100644 phpstan.neon create mode 100644 tests/phpstan/phpunit-bootstrap.php diff --git a/.github/workflows/portfolio-audit.yml b/.github/workflows/portfolio-audit.yml index 91f3bc7..b577c5e 100644 --- a/.github/workflows/portfolio-audit.yml +++ b/.github/workflows/portfolio-audit.yml @@ -21,7 +21,7 @@ jobs: with: php-version: '8.4' coverage: none - tools: phpunit:11.5 + tools: phpunit:11.5, phpstan - name: Check PHP example syntax shell: bash @@ -34,6 +34,9 @@ jobs: - name: Run public PHPUnit examples run: phpunit + - name: Run PHPStan on public examples + run: phpstan analyse --configuration=phpstan.neon --no-progress + - name: Run public showcase safety audit run: python tests/showcase_audit.py diff --git a/.gitignore b/.gitignore index 6059378..7c895d9 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ /vendor/ /.phpunit.cache/ +/.phpstan.cache/ /__pycache__/ **/__pycache__/ /.env diff --git a/CHANGELOG.md b/CHANGELOG.md index 3256148..6839e62 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ ## 2026-06-15 +- Added PHPStan `level: max` for public PHP examples and wired it into the Portfolio Audit workflow. - Added a Senior Symfony review guide that explains architecture boundaries, trade-offs, operational evidence, and reviewer prompts. - Added PHPUnit coverage for the public next-due-question example. - Added a repeatable public showcase safety audit for env-file policy, obvious secret patterns, private-code path leakage, local links, screenshots, wording consistency, and PHPUnit XML parsing. diff --git a/README.md b/README.md index 084ed4d..e4e7218 100644 --- a/README.md +++ b/README.md @@ -188,6 +188,7 @@ Public showcase checks: ```bash phpunit +phpstan analyse --configuration=phpstan.neon python tests/showcase_audit.py ``` @@ -213,6 +214,7 @@ OK Additional checks performed: - public showcase safety audit for env-file policy, obvious secret patterns, private-code path leakage, local links, screenshots, and wording consistency +- PHPStan `level: max` for public PHP examples - PHP syntax check across 180 files - Twig lint across 38 templates - YAML lint across 18 config files diff --git a/docs/evidence/public-live-readiness-2026-06-15.md b/docs/evidence/public-live-readiness-2026-06-15.md index a43e695..7ff664c 100644 --- a/docs/evidence/public-live-readiness-2026-06-15.md +++ b/docs/evidence/public-live-readiness-2026-06-15.md @@ -19,6 +19,7 @@ Local checks performed: ```text PHP syntax check for examples and tests: OK PHPUnit 11.5.55 on PHP 8.4.21: OK, 7 tests, 10 assertions +PHPStan 2.2.2 level max for public examples: OK Local Markdown links: OK Public wording consistency: OK Environment file policy: OK diff --git a/docs/quality-report.md b/docs/quality-report.md index 64375df..93b7f42 100644 --- a/docs/quality-report.md +++ b/docs/quality-report.md @@ -27,6 +27,14 @@ public wording is consistent phpunit.xml is valid XML ``` +Public PHPStan check: + +```text +phpstan analyse --configuration=phpstan.neon + +[OK] No errors +``` + Current private test suite: ```text @@ -78,6 +86,7 @@ Performed checks: - Doctrine mapping validation - live smoke checks against production routes - public showcase safety audit for secrets, private-code leaks, local links, screenshots, and wording consistency +- PHPStan `level: max` for public PHP examples Detailed evidence: diff --git a/docs/senior-symfony-review.md b/docs/senior-symfony-review.md index 6d354ed..2f9a04d 100644 --- a/docs/senior-symfony-review.md +++ b/docs/senior-symfony-review.md @@ -39,6 +39,7 @@ SkillBuilder is positioned as a backend-driven Symfony business application, not | Shopware sync is admin-triggered and idempotent | Repeated imports should update existing products and deactivate obsolete ones, not create duplicates | [Shopware bridge](shopware-bridge.md) | | Secrets stay outside Git | Public showcase has placeholders only; production values live in environment or private config outside the repo tree | [README](../README.md), [.env.example](../.env.example) | | Public repository has its own safety audit | The showcase should prove it is safe to publish, not merely claim it | [`tests/showcase_audit.py`](../tests/showcase_audit.py) | +| Static analysis is part of the public proof | PHPStan runs at `level: max` for the representative PHP examples without requiring private code | [`phpstan.neon`](../phpstan.neon), [Quality report](quality-report.md) | ## Symfony-Specific Signals @@ -51,6 +52,7 @@ The private application work represented here includes: - Doctrine ORM entities, repositories, mapping validation, and migration hygiene - service-layer business rules for learning progress, due-question selection, imports, statistics, exports, and external API synchronization - PHPUnit tests for core learning behavior +- PHPStan for public example static analysis - production smoke checks for public and authenticated routes The public examples are reduced, but they mirror the shape of the real implementation: small services with deterministic inputs, explicit state transitions, and tests around business rules. diff --git a/phpstan.neon b/phpstan.neon new file mode 100644 index 0000000..1bdaa8c --- /dev/null +++ b/phpstan.neon @@ -0,0 +1,6 @@ +parameters: + level: max + paths: + - examples + bootstrapFiles: + - tests/phpstan/phpunit-bootstrap.php diff --git a/tests/phpstan/phpunit-bootstrap.php b/tests/phpstan/phpunit-bootstrap.php new file mode 100644 index 0000000..535d1c9 --- /dev/null +++ b/tests/phpstan/phpunit-bootstrap.php @@ -0,0 +1,26 @@ + Date: Mon, 15 Jun 2026 20:52:45 +0200 Subject: [PATCH 4/4] Add Composer verify workflow --- .github/workflows/portfolio-audit.yml | 131 +- CHANGELOG.md | 3 + README.md | 8 +- composer.json | 26 + composer.lock | 1866 +++++++++++++++++ .../0001-public-private-repository-split.md | 39 + ...002-composer-verify-for-public-showcase.md | 46 + docs/adr/0003-shopware-sync-boundary.md | 29 + .../public-live-readiness-2026-06-15.md | 7 + docs/quality-report.md | 16 +- docs/security-privacy.md | 63 + docs/senior-symfony-review.md | 5 +- phpstan.neon | 2 - tests/php_lint.py | 48 + tests/phpstan/phpunit-bootstrap.php | 26 - tests/public_smoke.py | 60 + tests/showcase_audit.py | 38 +- 17 files changed, 2251 insertions(+), 162 deletions(-) create mode 100644 composer.json create mode 100644 composer.lock create mode 100644 docs/adr/0001-public-private-repository-split.md create mode 100644 docs/adr/0002-composer-verify-for-public-showcase.md create mode 100644 docs/adr/0003-shopware-sync-boundary.md create mode 100644 docs/security-privacy.md create mode 100644 tests/php_lint.py delete mode 100644 tests/phpstan/phpunit-bootstrap.php create mode 100644 tests/public_smoke.py diff --git a/.github/workflows/portfolio-audit.yml b/.github/workflows/portfolio-audit.yml index b577c5e..f60724f 100644 --- a/.github/workflows/portfolio-audit.yml +++ b/.github/workflows/portfolio-audit.yml @@ -21,131 +21,12 @@ jobs: with: php-version: '8.4' coverage: none - tools: phpunit:11.5, phpstan - - name: Check PHP example syntax - shell: bash - run: | - set -euo pipefail - while IFS= read -r file; do - php -l "$file" - done < <(find examples tests -name '*.php' -type f | sort) + - name: Validate Composer config + run: composer validate --strict --no-check-publish - - name: Run public PHPUnit examples - run: phpunit + - name: Install Composer dependencies + run: composer install --no-interaction --prefer-dist --no-progress - - name: Run PHPStan on public examples - run: phpstan analyse --configuration=phpstan.neon --no-progress - - - name: Run public showcase safety audit - run: python tests/showcase_audit.py - - - name: Check local Markdown links - shell: python - run: | - from pathlib import Path - import re - import sys - - failures = [] - pattern = re.compile(r'!?\[[^\]]*\]\(([^)]+)\)') - - for path in Path('.').rglob('*.md'): - text = path.read_text(encoding='utf-8') - for match in pattern.finditer(text): - target = match.group(1).strip() - if not target or target.startswith(('http://', 'https://', 'mailto:', '#')): - continue - clean = target.split('#', 1)[0] - if not clean: - continue - if not (path.parent / clean).exists(): - failures.append(f'{path}: missing local link target {target}') - - if failures: - print('\n'.join(failures)) - sys.exit(1) - - - name: Check public wording consistency - shell: python - run: | - from pathlib import Path - import sys - - forbidden = [ - 'Kapitel als ' + 'Kategorien', - 'Kategorien ' + 'exportieren', - 'Kapitel werden ' + 'Kategorien', - 'chapters become ' + 'categories', - 'Lessons als Produkte, ' + 'Kapitel', - ] - suffixes = {'.md', '.php', '.svg', '.txt', '.yml', '.yaml'} - failures = [] - - for path in Path('.').rglob('*'): - if '.git' in path.parts: - continue - if path.suffix.lower() not in suffixes: - continue - text = path.read_text(encoding='utf-8', errors='ignore') - lower = text.lower() - for phrase in forbidden: - if phrase.lower() in lower: - failures.append(f'{path}: outdated wording found') - - if failures: - print('\n'.join(failures)) - sys.exit(1) - - - name: Check environment file policy - shell: python - run: | - from pathlib import Path - import sys - - allowed = {'.env.example'} - forbidden = [] - - for path in Path('.').rglob('.env*'): - if '.git' in path.parts: - continue - if path.name not in allowed: - forbidden.append(str(path)) - - if forbidden: - print('Real environment files must not be committed:') - print('\n'.join(forbidden)) - sys.exit(1) - - - name: Check for obvious secret patterns - shell: python - run: | - from pathlib import Path - import re - import sys - - patterns = [ - re.compile(r'github_pat_[A-Za-z0-9_]+'), - re.compile(r'ghp_[A-Za-z0-9_]+'), - re.compile(r'sk-[A-Za-z0-9]{20,}'), - re.compile(r'APP_SECRET\s*='), - re.compile(r'DATABASE_URL\s*='), - ] - suffixes = {'.md', '.php', '.svg', '.txt', '.yml', '.yaml'} - failures = [] - - for path in Path('.').rglob('*'): - if '.git' in path.parts or '.github' in path.parts: - continue - if path.name == '.env.example': - continue - if path.suffix.lower() not in suffixes: - continue - text = path.read_text(encoding='utf-8', errors='ignore') - for pattern in patterns: - if pattern.search(text): - failures.append(f'{path}: possible secret pattern found') - - if failures: - print('\n'.join(failures)) - sys.exit(1) + - name: Run public showcase verification + run: composer verify diff --git a/CHANGELOG.md b/CHANGELOG.md index 6839e62..8efb754 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,9 @@ ## 2026-06-15 +- Added Composer-based reproducible verification with `composer verify` and a separate `composer smoke:public` live check. +- Added ADRs for the public/private repository split, Composer verification, and the Shopware synchronization boundary. +- Added a security and privacy boundary document for public showcase review. - Added PHPStan `level: max` for public PHP examples and wired it into the Portfolio Audit workflow. - Added a Senior Symfony review guide that explains architecture boundaries, trade-offs, operational evidence, and reviewer prompts. - Added PHPUnit coverage for the public next-due-question example. diff --git a/README.md b/README.md index e4e7218..9f4dd2c 100644 --- a/README.md +++ b/README.md @@ -141,6 +141,8 @@ Only published SkillBuilder lessons are synchronized as Shopware products. Draft - [PHPStan audit evidence](docs/phpstan-audit-2026-06-05.md) - [Evidence index](docs/evidence/README.md) - [Senior Symfony review guide](docs/senior-symfony-review.md) +- [Security and privacy boundary](docs/security-privacy.md) +- [Architecture decisions](docs/adr/) - [Demo script](docs/demo-script.md) - [Changelog](CHANGELOG.md) - [Demo checklist](docs/demo-checklist.md) @@ -187,9 +189,9 @@ This repository does not include: Public showcase checks: ```bash -phpunit -phpstan analyse --configuration=phpstan.neon -python tests/showcase_audit.py +composer install +composer verify +composer smoke:public ``` Current public example test result: diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..d61cddf --- /dev/null +++ b/composer.json @@ -0,0 +1,26 @@ +{ + "name": "roadynet/skillbuilder-showcase", + "description": "Public portfolio evidence for the private SkillBuilder Symfony learning platform.", + "type": "project", + "license": "proprietary", + "require": { + "php": ">=8.4" + }, + "require-dev": { + "phpstan/phpstan": "^2.2", + "phpunit/phpunit": "^11.5" + }, + "scripts": { + "lint:php": "python tests/php_lint.py", + "analyse": "phpstan analyse --configuration=phpstan.neon --no-progress", + "test": "phpunit", + "audit:showcase": "python tests/showcase_audit.py", + "smoke:public": "python tests/public_smoke.py", + "verify": [ + "@lint:php", + "@analyse", + "@test", + "@audit:showcase" + ] + } +} diff --git a/composer.lock b/composer.lock new file mode 100644 index 0000000..bbac008 --- /dev/null +++ b/composer.lock @@ -0,0 +1,1866 @@ +{ + "_readme": [ + "This file locks the dependencies of your project to a known state", + "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", + "This file is @generated automatically" + ], + "content-hash": "d79c2e08df6b9f871c65c132de0fb825", + "packages": [], + "packages-dev": [ + { + "name": "myclabs/deep-copy", + "version": "1.13.4", + "source": { + "type": "git", + "url": "https://github.com/myclabs/DeepCopy.git", + "reference": "07d290f0c47959fd5eed98c95ee5602db07e0b6a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/07d290f0c47959fd5eed98c95ee5602db07e0b6a", + "reference": "07d290f0c47959fd5eed98c95ee5602db07e0b6a", + "shasum": "" + }, + "require": { + "php": "^7.1 || ^8.0" + }, + "conflict": { + "doctrine/collections": "<1.6.8", + "doctrine/common": "<2.13.3 || >=3 <3.2.2" + }, + "require-dev": { + "doctrine/collections": "^1.6.8", + "doctrine/common": "^2.13.3 || ^3.2.2", + "phpspec/prophecy": "^1.10", + "phpunit/phpunit": "^7.5.20 || ^8.5.23 || ^9.5.13" + }, + "type": "library", + "autoload": { + "files": [ + "src/DeepCopy/deep_copy.php" + ], + "psr-4": { + "DeepCopy\\": "src/DeepCopy/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Create deep copies (clones) of your objects", + "keywords": [ + "clone", + "copy", + "duplicate", + "object", + "object graph" + ], + "support": { + "issues": "https://github.com/myclabs/DeepCopy/issues", + "source": "https://github.com/myclabs/DeepCopy/tree/1.13.4" + }, + "funding": [ + { + "url": "https://tidelift.com/funding/github/packagist/myclabs/deep-copy", + "type": "tidelift" + } + ], + "time": "2025-08-01T08:46:24+00:00" + }, + { + "name": "nikic/php-parser", + "version": "v5.7.0", + "source": { + "type": "git", + "url": "https://github.com/nikic/PHP-Parser.git", + "reference": "dca41cd15c2ac9d055ad70dbfd011130757d1f82" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/dca41cd15c2ac9d055ad70dbfd011130757d1f82", + "reference": "dca41cd15c2ac9d055ad70dbfd011130757d1f82", + "shasum": "" + }, + "require": { + "ext-ctype": "*", + "ext-json": "*", + "ext-tokenizer": "*", + "php": ">=7.4" + }, + "require-dev": { + "ircmaxell/php-yacc": "^0.0.7", + "phpunit/phpunit": "^9.0" + }, + "bin": [ + "bin/php-parse" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "5.x-dev" + } + }, + "autoload": { + "psr-4": { + "PhpParser\\": "lib/PhpParser" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Nikita Popov" + } + ], + "description": "A PHP parser written in PHP", + "keywords": [ + "parser", + "php" + ], + "support": { + "issues": "https://github.com/nikic/PHP-Parser/issues", + "source": "https://github.com/nikic/PHP-Parser/tree/v5.7.0" + }, + "time": "2025-12-06T11:56:16+00:00" + }, + { + "name": "phar-io/manifest", + "version": "2.0.4", + "source": { + "type": "git", + "url": "https://github.com/phar-io/manifest.git", + "reference": "54750ef60c58e43759730615a392c31c80e23176" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phar-io/manifest/zipball/54750ef60c58e43759730615a392c31c80e23176", + "reference": "54750ef60c58e43759730615a392c31c80e23176", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-libxml": "*", + "ext-phar": "*", + "ext-xmlwriter": "*", + "phar-io/version": "^3.0.1", + "php": "^7.2 || ^8.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Arne Blankerts", + "email": "arne@blankerts.de", + "role": "Developer" + }, + { + "name": "Sebastian Heuer", + "email": "sebastian@phpeople.de", + "role": "Developer" + }, + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "Developer" + } + ], + "description": "Component for reading phar.io manifest information from a PHP Archive (PHAR)", + "support": { + "issues": "https://github.com/phar-io/manifest/issues", + "source": "https://github.com/phar-io/manifest/tree/2.0.4" + }, + "funding": [ + { + "url": "https://github.com/theseer", + "type": "github" + } + ], + "time": "2024-03-03T12:33:53+00:00" + }, + { + "name": "phar-io/version", + "version": "3.2.1", + "source": { + "type": "git", + "url": "https://github.com/phar-io/version.git", + "reference": "4f7fd7836c6f332bb2933569e566a0d6c4cbed74" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phar-io/version/zipball/4f7fd7836c6f332bb2933569e566a0d6c4cbed74", + "reference": "4f7fd7836c6f332bb2933569e566a0d6c4cbed74", + "shasum": "" + }, + "require": { + "php": "^7.2 || ^8.0" + }, + "type": "library", + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Arne Blankerts", + "email": "arne@blankerts.de", + "role": "Developer" + }, + { + "name": "Sebastian Heuer", + "email": "sebastian@phpeople.de", + "role": "Developer" + }, + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "Developer" + } + ], + "description": "Library for handling version information and constraints", + "support": { + "issues": "https://github.com/phar-io/version/issues", + "source": "https://github.com/phar-io/version/tree/3.2.1" + }, + "time": "2022-02-21T01:04:05+00:00" + }, + { + "name": "phpstan/phpstan", + "version": "2.2.2", + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpstan/phpstan/zipball/e5cc34d491a90e79c216d824f60fe21fd4d93bd6", + "reference": "e5cc34d491a90e79c216d824f60fe21fd4d93bd6", + "shasum": "" + }, + "require": { + "php": "^7.4|^8.0" + }, + "conflict": { + "phpstan/phpstan-shim": "*" + }, + "bin": [ + "phpstan", + "phpstan.phar" + ], + "type": "library", + "autoload": { + "files": [ + "bootstrap.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Ondřej Mirtes" + }, + { + "name": "Markus Staab" + }, + { + "name": "Vincent Langlet" + } + ], + "description": "PHPStan - PHP Static Analysis Tool", + "keywords": [ + "dev", + "static analysis" + ], + "support": { + "docs": "https://phpstan.org/user-guide/getting-started", + "forum": "https://github.com/phpstan/phpstan/discussions", + "issues": "https://github.com/phpstan/phpstan/issues", + "security": "https://github.com/phpstan/phpstan/security/policy", + "source": "https://github.com/phpstan/phpstan-src" + }, + "funding": [ + { + "url": "https://github.com/ondrejmirtes", + "type": "github" + }, + { + "url": "https://github.com/phpstan", + "type": "github" + } + ], + "time": "2026-06-05T09:00:01+00:00" + }, + { + "name": "phpunit/php-code-coverage", + "version": "11.0.12", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-code-coverage.git", + "reference": "2c1ed04922802c15e1de5d7447b4856de949cf56" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/2c1ed04922802c15e1de5d7447b4856de949cf56", + "reference": "2c1ed04922802c15e1de5d7447b4856de949cf56", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-libxml": "*", + "ext-xmlwriter": "*", + "nikic/php-parser": "^5.7.0", + "php": ">=8.2", + "phpunit/php-file-iterator": "^5.1.0", + "phpunit/php-text-template": "^4.0.1", + "sebastian/code-unit-reverse-lookup": "^4.0.1", + "sebastian/complexity": "^4.0.1", + "sebastian/environment": "^7.2.1", + "sebastian/lines-of-code": "^3.0.1", + "sebastian/version": "^5.0.2", + "theseer/tokenizer": "^1.3.1" + }, + "require-dev": { + "phpunit/phpunit": "^11.5.46" + }, + "suggest": { + "ext-pcov": "PHP extension that provides line coverage", + "ext-xdebug": "PHP extension that provides line coverage as well as branch and path coverage" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "11.0.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library that provides collection, processing, and rendering functionality for PHP code coverage information.", + "homepage": "https://github.com/sebastianbergmann/php-code-coverage", + "keywords": [ + "coverage", + "testing", + "xunit" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-code-coverage/issues", + "security": "https://github.com/sebastianbergmann/php-code-coverage/security/policy", + "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/11.0.12" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/phpunit/php-code-coverage", + "type": "tidelift" + } + ], + "time": "2025-12-24T07:01:01+00:00" + }, + { + "name": "phpunit/php-file-iterator", + "version": "5.1.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-file-iterator.git", + "reference": "2f3a64888c814fc235386b7387dd5b5ed92ad903" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/2f3a64888c814fc235386b7387dd5b5ed92ad903", + "reference": "2f3a64888c814fc235386b7387dd5b5ed92ad903", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "require-dev": { + "phpunit/phpunit": "^11.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "5.1-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "FilterIterator implementation that filters files based on a list of suffixes.", + "homepage": "https://github.com/sebastianbergmann/php-file-iterator/", + "keywords": [ + "filesystem", + "iterator" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-file-iterator/issues", + "security": "https://github.com/sebastianbergmann/php-file-iterator/security/policy", + "source": "https://github.com/sebastianbergmann/php-file-iterator/tree/5.1.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/phpunit/php-file-iterator", + "type": "tidelift" + } + ], + "time": "2026-02-02T13:52:54+00:00" + }, + { + "name": "phpunit/php-invoker", + "version": "5.0.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-invoker.git", + "reference": "c1ca3814734c07492b3d4c5f794f4b0995333da2" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-invoker/zipball/c1ca3814734c07492b3d4c5f794f4b0995333da2", + "reference": "c1ca3814734c07492b3d4c5f794f4b0995333da2", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "require-dev": { + "ext-pcntl": "*", + "phpunit/phpunit": "^11.0" + }, + "suggest": { + "ext-pcntl": "*" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "5.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Invoke callables with a timeout", + "homepage": "https://github.com/sebastianbergmann/php-invoker/", + "keywords": [ + "process" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-invoker/issues", + "security": "https://github.com/sebastianbergmann/php-invoker/security/policy", + "source": "https://github.com/sebastianbergmann/php-invoker/tree/5.0.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-07-03T05:07:44+00:00" + }, + { + "name": "phpunit/php-text-template", + "version": "4.0.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-text-template.git", + "reference": "3e0404dc6b300e6bf56415467ebcb3fe4f33e964" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-text-template/zipball/3e0404dc6b300e6bf56415467ebcb3fe4f33e964", + "reference": "3e0404dc6b300e6bf56415467ebcb3fe4f33e964", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "require-dev": { + "phpunit/phpunit": "^11.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "4.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Simple template engine.", + "homepage": "https://github.com/sebastianbergmann/php-text-template/", + "keywords": [ + "template" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-text-template/issues", + "security": "https://github.com/sebastianbergmann/php-text-template/security/policy", + "source": "https://github.com/sebastianbergmann/php-text-template/tree/4.0.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-07-03T05:08:43+00:00" + }, + { + "name": "phpunit/php-timer", + "version": "7.0.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-timer.git", + "reference": "3b415def83fbcb41f991d9ebf16ae4ad8b7837b3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-timer/zipball/3b415def83fbcb41f991d9ebf16ae4ad8b7837b3", + "reference": "3b415def83fbcb41f991d9ebf16ae4ad8b7837b3", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "require-dev": { + "phpunit/phpunit": "^11.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "7.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Utility class for timing", + "homepage": "https://github.com/sebastianbergmann/php-timer/", + "keywords": [ + "timer" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-timer/issues", + "security": "https://github.com/sebastianbergmann/php-timer/security/policy", + "source": "https://github.com/sebastianbergmann/php-timer/tree/7.0.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-07-03T05:09:35+00:00" + }, + { + "name": "phpunit/phpunit", + "version": "11.5.55", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/phpunit.git", + "reference": "adc7262fccc12de2b30f12a8aa0b33775d814f00" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/adc7262fccc12de2b30f12a8aa0b33775d814f00", + "reference": "adc7262fccc12de2b30f12a8aa0b33775d814f00", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-json": "*", + "ext-libxml": "*", + "ext-mbstring": "*", + "ext-xml": "*", + "ext-xmlwriter": "*", + "myclabs/deep-copy": "^1.13.4", + "phar-io/manifest": "^2.0.4", + "phar-io/version": "^3.2.1", + "php": ">=8.2", + "phpunit/php-code-coverage": "^11.0.12", + "phpunit/php-file-iterator": "^5.1.1", + "phpunit/php-invoker": "^5.0.1", + "phpunit/php-text-template": "^4.0.1", + "phpunit/php-timer": "^7.0.1", + "sebastian/cli-parser": "^3.0.2", + "sebastian/code-unit": "^3.0.3", + "sebastian/comparator": "^6.3.3", + "sebastian/diff": "^6.0.2", + "sebastian/environment": "^7.2.1", + "sebastian/exporter": "^6.3.2", + "sebastian/global-state": "^7.0.2", + "sebastian/object-enumerator": "^6.0.1", + "sebastian/recursion-context": "^6.0.3", + "sebastian/type": "^5.1.3", + "sebastian/version": "^5.0.2", + "staabm/side-effects-detector": "^1.0.5" + }, + "suggest": { + "ext-soap": "To be able to generate mocks based on WSDL files" + }, + "bin": [ + "phpunit" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "11.5-dev" + } + }, + "autoload": { + "files": [ + "src/Framework/Assert/Functions.php" + ], + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "The PHP Unit Testing framework.", + "homepage": "https://phpunit.de/", + "keywords": [ + "phpunit", + "testing", + "xunit" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/phpunit/issues", + "security": "https://github.com/sebastianbergmann/phpunit/security/policy", + "source": "https://github.com/sebastianbergmann/phpunit/tree/11.5.55" + }, + "funding": [ + { + "url": "https://phpunit.de/sponsors.html", + "type": "custom" + }, + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/phpunit/phpunit", + "type": "tidelift" + } + ], + "time": "2026-02-18T12:37:06+00:00" + }, + { + "name": "sebastian/cli-parser", + "version": "3.0.2", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/cli-parser.git", + "reference": "15c5dd40dc4f38794d383bb95465193f5e0ae180" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/cli-parser/zipball/15c5dd40dc4f38794d383bb95465193f5e0ae180", + "reference": "15c5dd40dc4f38794d383bb95465193f5e0ae180", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "require-dev": { + "phpunit/phpunit": "^11.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library for parsing CLI options", + "homepage": "https://github.com/sebastianbergmann/cli-parser", + "support": { + "issues": "https://github.com/sebastianbergmann/cli-parser/issues", + "security": "https://github.com/sebastianbergmann/cli-parser/security/policy", + "source": "https://github.com/sebastianbergmann/cli-parser/tree/3.0.2" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-07-03T04:41:36+00:00" + }, + { + "name": "sebastian/code-unit", + "version": "3.0.3", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/code-unit.git", + "reference": "54391c61e4af8078e5b276ab082b6d3c54c9ad64" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/code-unit/zipball/54391c61e4af8078e5b276ab082b6d3c54c9ad64", + "reference": "54391c61e4af8078e5b276ab082b6d3c54c9ad64", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "require-dev": { + "phpunit/phpunit": "^11.5" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Collection of value objects that represent the PHP code units", + "homepage": "https://github.com/sebastianbergmann/code-unit", + "support": { + "issues": "https://github.com/sebastianbergmann/code-unit/issues", + "security": "https://github.com/sebastianbergmann/code-unit/security/policy", + "source": "https://github.com/sebastianbergmann/code-unit/tree/3.0.3" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2025-03-19T07:56:08+00:00" + }, + { + "name": "sebastian/code-unit-reverse-lookup", + "version": "4.0.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/code-unit-reverse-lookup.git", + "reference": "183a9b2632194febd219bb9246eee421dad8d45e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/code-unit-reverse-lookup/zipball/183a9b2632194febd219bb9246eee421dad8d45e", + "reference": "183a9b2632194febd219bb9246eee421dad8d45e", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "require-dev": { + "phpunit/phpunit": "^11.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "4.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Looks up which function or method a line of code belongs to", + "homepage": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/", + "support": { + "issues": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/issues", + "security": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/security/policy", + "source": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/tree/4.0.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-07-03T04:45:54+00:00" + }, + { + "name": "sebastian/comparator", + "version": "6.3.3", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/comparator.git", + "reference": "2c95e1e86cb8dd41beb8d502057d1081ccc8eca9" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/2c95e1e86cb8dd41beb8d502057d1081ccc8eca9", + "reference": "2c95e1e86cb8dd41beb8d502057d1081ccc8eca9", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-mbstring": "*", + "php": ">=8.2", + "sebastian/diff": "^6.0", + "sebastian/exporter": "^6.0" + }, + "require-dev": { + "phpunit/phpunit": "^11.4" + }, + "suggest": { + "ext-bcmath": "For comparing BcMath\\Number objects" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "6.3-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Jeff Welch", + "email": "whatthejeff@gmail.com" + }, + { + "name": "Volker Dusch", + "email": "github@wallbash.com" + }, + { + "name": "Bernhard Schussek", + "email": "bschussek@2bepublished.at" + } + ], + "description": "Provides the functionality to compare PHP values for equality", + "homepage": "https://github.com/sebastianbergmann/comparator", + "keywords": [ + "comparator", + "compare", + "equality" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/comparator/issues", + "security": "https://github.com/sebastianbergmann/comparator/security/policy", + "source": "https://github.com/sebastianbergmann/comparator/tree/6.3.3" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/comparator", + "type": "tidelift" + } + ], + "time": "2026-01-24T09:26:40+00:00" + }, + { + "name": "sebastian/complexity", + "version": "4.0.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/complexity.git", + "reference": "ee41d384ab1906c68852636b6de493846e13e5a0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/complexity/zipball/ee41d384ab1906c68852636b6de493846e13e5a0", + "reference": "ee41d384ab1906c68852636b6de493846e13e5a0", + "shasum": "" + }, + "require": { + "nikic/php-parser": "^5.0", + "php": ">=8.2" + }, + "require-dev": { + "phpunit/phpunit": "^11.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "4.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library for calculating the complexity of PHP code units", + "homepage": "https://github.com/sebastianbergmann/complexity", + "support": { + "issues": "https://github.com/sebastianbergmann/complexity/issues", + "security": "https://github.com/sebastianbergmann/complexity/security/policy", + "source": "https://github.com/sebastianbergmann/complexity/tree/4.0.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-07-03T04:49:50+00:00" + }, + { + "name": "sebastian/diff", + "version": "6.0.2", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/diff.git", + "reference": "b4ccd857127db5d41a5b676f24b51371d76d8544" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/b4ccd857127db5d41a5b676f24b51371d76d8544", + "reference": "b4ccd857127db5d41a5b676f24b51371d76d8544", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "require-dev": { + "phpunit/phpunit": "^11.0", + "symfony/process": "^4.2 || ^5" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "6.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Kore Nordmann", + "email": "mail@kore-nordmann.de" + } + ], + "description": "Diff implementation", + "homepage": "https://github.com/sebastianbergmann/diff", + "keywords": [ + "diff", + "udiff", + "unidiff", + "unified diff" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/diff/issues", + "security": "https://github.com/sebastianbergmann/diff/security/policy", + "source": "https://github.com/sebastianbergmann/diff/tree/6.0.2" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-07-03T04:53:05+00:00" + }, + { + "name": "sebastian/environment", + "version": "7.2.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/environment.git", + "reference": "a5c75038693ad2e8d4b6c15ba2403532647830c4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/a5c75038693ad2e8d4b6c15ba2403532647830c4", + "reference": "a5c75038693ad2e8d4b6c15ba2403532647830c4", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "require-dev": { + "phpunit/phpunit": "^11.3" + }, + "suggest": { + "ext-posix": "*" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "7.2-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Provides functionality to handle HHVM/PHP environments", + "homepage": "https://github.com/sebastianbergmann/environment", + "keywords": [ + "Xdebug", + "environment", + "hhvm" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/environment/issues", + "security": "https://github.com/sebastianbergmann/environment/security/policy", + "source": "https://github.com/sebastianbergmann/environment/tree/7.2.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/environment", + "type": "tidelift" + } + ], + "time": "2025-05-21T11:55:47+00:00" + }, + { + "name": "sebastian/exporter", + "version": "6.3.2", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/exporter.git", + "reference": "70a298763b40b213ec087c51c739efcaa90bcd74" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/70a298763b40b213ec087c51c739efcaa90bcd74", + "reference": "70a298763b40b213ec087c51c739efcaa90bcd74", + "shasum": "" + }, + "require": { + "ext-mbstring": "*", + "php": ">=8.2", + "sebastian/recursion-context": "^6.0" + }, + "require-dev": { + "phpunit/phpunit": "^11.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "6.3-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Jeff Welch", + "email": "whatthejeff@gmail.com" + }, + { + "name": "Volker Dusch", + "email": "github@wallbash.com" + }, + { + "name": "Adam Harvey", + "email": "aharvey@php.net" + }, + { + "name": "Bernhard Schussek", + "email": "bschussek@gmail.com" + } + ], + "description": "Provides the functionality to export PHP variables for visualization", + "homepage": "https://www.github.com/sebastianbergmann/exporter", + "keywords": [ + "export", + "exporter" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/exporter/issues", + "security": "https://github.com/sebastianbergmann/exporter/security/policy", + "source": "https://github.com/sebastianbergmann/exporter/tree/6.3.2" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/exporter", + "type": "tidelift" + } + ], + "time": "2025-09-24T06:12:51+00:00" + }, + { + "name": "sebastian/global-state", + "version": "7.0.2", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/global-state.git", + "reference": "3be331570a721f9a4b5917f4209773de17f747d7" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/3be331570a721f9a4b5917f4209773de17f747d7", + "reference": "3be331570a721f9a4b5917f4209773de17f747d7", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "sebastian/object-reflector": "^4.0", + "sebastian/recursion-context": "^6.0" + }, + "require-dev": { + "ext-dom": "*", + "phpunit/phpunit": "^11.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "7.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Snapshotting of global state", + "homepage": "https://www.github.com/sebastianbergmann/global-state", + "keywords": [ + "global state" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/global-state/issues", + "security": "https://github.com/sebastianbergmann/global-state/security/policy", + "source": "https://github.com/sebastianbergmann/global-state/tree/7.0.2" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-07-03T04:57:36+00:00" + }, + { + "name": "sebastian/lines-of-code", + "version": "3.0.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/lines-of-code.git", + "reference": "d36ad0d782e5756913e42ad87cb2890f4ffe467a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/lines-of-code/zipball/d36ad0d782e5756913e42ad87cb2890f4ffe467a", + "reference": "d36ad0d782e5756913e42ad87cb2890f4ffe467a", + "shasum": "" + }, + "require": { + "nikic/php-parser": "^5.0", + "php": ">=8.2" + }, + "require-dev": { + "phpunit/phpunit": "^11.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library for counting the lines of code in PHP source code", + "homepage": "https://github.com/sebastianbergmann/lines-of-code", + "support": { + "issues": "https://github.com/sebastianbergmann/lines-of-code/issues", + "security": "https://github.com/sebastianbergmann/lines-of-code/security/policy", + "source": "https://github.com/sebastianbergmann/lines-of-code/tree/3.0.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-07-03T04:58:38+00:00" + }, + { + "name": "sebastian/object-enumerator", + "version": "6.0.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/object-enumerator.git", + "reference": "f5b498e631a74204185071eb41f33f38d64608aa" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/object-enumerator/zipball/f5b498e631a74204185071eb41f33f38d64608aa", + "reference": "f5b498e631a74204185071eb41f33f38d64608aa", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "sebastian/object-reflector": "^4.0", + "sebastian/recursion-context": "^6.0" + }, + "require-dev": { + "phpunit/phpunit": "^11.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "6.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Traverses array structures and object graphs to enumerate all referenced objects", + "homepage": "https://github.com/sebastianbergmann/object-enumerator/", + "support": { + "issues": "https://github.com/sebastianbergmann/object-enumerator/issues", + "security": "https://github.com/sebastianbergmann/object-enumerator/security/policy", + "source": "https://github.com/sebastianbergmann/object-enumerator/tree/6.0.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-07-03T05:00:13+00:00" + }, + { + "name": "sebastian/object-reflector", + "version": "4.0.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/object-reflector.git", + "reference": "6e1a43b411b2ad34146dee7524cb13a068bb35f9" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/object-reflector/zipball/6e1a43b411b2ad34146dee7524cb13a068bb35f9", + "reference": "6e1a43b411b2ad34146dee7524cb13a068bb35f9", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "require-dev": { + "phpunit/phpunit": "^11.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "4.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Allows reflection of object attributes, including inherited and non-public ones", + "homepage": "https://github.com/sebastianbergmann/object-reflector/", + "support": { + "issues": "https://github.com/sebastianbergmann/object-reflector/issues", + "security": "https://github.com/sebastianbergmann/object-reflector/security/policy", + "source": "https://github.com/sebastianbergmann/object-reflector/tree/4.0.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-07-03T05:01:32+00:00" + }, + { + "name": "sebastian/recursion-context", + "version": "6.0.3", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/recursion-context.git", + "reference": "f6458abbf32a6c8174f8f26261475dc133b3d9dc" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/f6458abbf32a6c8174f8f26261475dc133b3d9dc", + "reference": "f6458abbf32a6c8174f8f26261475dc133b3d9dc", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "require-dev": { + "phpunit/phpunit": "^11.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "6.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Jeff Welch", + "email": "whatthejeff@gmail.com" + }, + { + "name": "Adam Harvey", + "email": "aharvey@php.net" + } + ], + "description": "Provides functionality to recursively process PHP variables", + "homepage": "https://github.com/sebastianbergmann/recursion-context", + "support": { + "issues": "https://github.com/sebastianbergmann/recursion-context/issues", + "security": "https://github.com/sebastianbergmann/recursion-context/security/policy", + "source": "https://github.com/sebastianbergmann/recursion-context/tree/6.0.3" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/recursion-context", + "type": "tidelift" + } + ], + "time": "2025-08-13T04:42:22+00:00" + }, + { + "name": "sebastian/type", + "version": "5.1.3", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/type.git", + "reference": "f77d2d4e78738c98d9a68d2596fe5e8fa380f449" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/type/zipball/f77d2d4e78738c98d9a68d2596fe5e8fa380f449", + "reference": "f77d2d4e78738c98d9a68d2596fe5e8fa380f449", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "require-dev": { + "phpunit/phpunit": "^11.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "5.1-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Collection of value objects that represent the types of the PHP type system", + "homepage": "https://github.com/sebastianbergmann/type", + "support": { + "issues": "https://github.com/sebastianbergmann/type/issues", + "security": "https://github.com/sebastianbergmann/type/security/policy", + "source": "https://github.com/sebastianbergmann/type/tree/5.1.3" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/type", + "type": "tidelift" + } + ], + "time": "2025-08-09T06:55:48+00:00" + }, + { + "name": "sebastian/version", + "version": "5.0.2", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/version.git", + "reference": "c687e3387b99f5b03b6caa64c74b63e2936ff874" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/version/zipball/c687e3387b99f5b03b6caa64c74b63e2936ff874", + "reference": "c687e3387b99f5b03b6caa64c74b63e2936ff874", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "5.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library that helps with managing the version number of Git-hosted PHP projects", + "homepage": "https://github.com/sebastianbergmann/version", + "support": { + "issues": "https://github.com/sebastianbergmann/version/issues", + "security": "https://github.com/sebastianbergmann/version/security/policy", + "source": "https://github.com/sebastianbergmann/version/tree/5.0.2" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-10-09T05:16:32+00:00" + }, + { + "name": "staabm/side-effects-detector", + "version": "1.0.5", + "source": { + "type": "git", + "url": "https://github.com/staabm/side-effects-detector.git", + "reference": "d8334211a140ce329c13726d4a715adbddd0a163" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/staabm/side-effects-detector/zipball/d8334211a140ce329c13726d4a715adbddd0a163", + "reference": "d8334211a140ce329c13726d4a715adbddd0a163", + "shasum": "" + }, + "require": { + "ext-tokenizer": "*", + "php": "^7.4 || ^8.0" + }, + "require-dev": { + "phpstan/extension-installer": "^1.4.3", + "phpstan/phpstan": "^1.12.6", + "phpunit/phpunit": "^9.6.21", + "symfony/var-dumper": "^5.4.43", + "tomasvotruba/type-coverage": "1.0.0", + "tomasvotruba/unused-public": "1.0.0" + }, + "type": "library", + "autoload": { + "classmap": [ + "lib/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "A static analysis tool to detect side effects in PHP code", + "keywords": [ + "static analysis" + ], + "support": { + "issues": "https://github.com/staabm/side-effects-detector/issues", + "source": "https://github.com/staabm/side-effects-detector/tree/1.0.5" + }, + "funding": [ + { + "url": "https://github.com/staabm", + "type": "github" + } + ], + "time": "2024-10-20T05:08:20+00:00" + }, + { + "name": "theseer/tokenizer", + "version": "1.3.1", + "source": { + "type": "git", + "url": "https://github.com/theseer/tokenizer.git", + "reference": "b7489ce515e168639d17feec34b8847c326b0b3c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/theseer/tokenizer/zipball/b7489ce515e168639d17feec34b8847c326b0b3c", + "reference": "b7489ce515e168639d17feec34b8847c326b0b3c", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-tokenizer": "*", + "ext-xmlwriter": "*", + "php": "^7.2 || ^8.0" + }, + "type": "library", + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Arne Blankerts", + "email": "arne@blankerts.de", + "role": "Developer" + } + ], + "description": "A small library for converting tokenized PHP source code into XML and potentially other formats", + "support": { + "issues": "https://github.com/theseer/tokenizer/issues", + "source": "https://github.com/theseer/tokenizer/tree/1.3.1" + }, + "funding": [ + { + "url": "https://github.com/theseer", + "type": "github" + } + ], + "time": "2025-11-17T20:03:58+00:00" + } + ], + "aliases": [], + "minimum-stability": "stable", + "stability-flags": [], + "prefer-stable": false, + "prefer-lowest": false, + "platform": { + "php": ">=8.4" + }, + "platform-dev": [], + "plugin-api-version": "2.6.0" +} diff --git a/docs/adr/0001-public-private-repository-split.md b/docs/adr/0001-public-private-repository-split.md new file mode 100644 index 0000000..e6ae091 --- /dev/null +++ b/docs/adr/0001-public-private-repository-split.md @@ -0,0 +1,39 @@ +# ADR 0001: Keep Public Showcase Separate From Private Application + +## Status + +Accepted + +## Context + +SkillBuilder is a real Symfony application with private source code, deployment configuration, credentials, user data boundaries, and business-specific implementation details. + +A public portfolio repository needs to prove engineering quality without publishing material that should remain private. + +## Decision + +Keep `roadynet/skillbuilder-showcase` as a public evidence repository, separate from the private production application. + +The public repository may contain: + +- architecture notes +- sanitized examples +- screenshots +- public quality evidence +- repeatable public checks +- demo-safe documentation + +The public repository must not contain: + +- private Symfony source directories +- production `.env` files +- credentials +- user data +- private deployment scripts +- generated exports or uploads + +## Consequences + +Reviewers cannot inspect the full private codebase here. Instead, they get a safe, repeatable, evidence-oriented view of the project. + +The showcase therefore needs stronger public checks than a normal readme-only portfolio repository: local link checks, secret-pattern checks, private-path guards, PHP syntax checks, PHPUnit, and PHPStan. diff --git a/docs/adr/0002-composer-verify-for-public-showcase.md b/docs/adr/0002-composer-verify-for-public-showcase.md new file mode 100644 index 0000000..3391b22 --- /dev/null +++ b/docs/adr/0002-composer-verify-for-public-showcase.md @@ -0,0 +1,46 @@ +# ADR 0002: Use Composer Verify For Public Showcase Checks + +## Status + +Accepted + +## Context + +The showcase has several independent checks: + +- PHP syntax checks +- PHPUnit example tests +- PHPStan static analysis +- public safety audit +- optional live smoke checks + +Running these as separate ad hoc commands is easy for the maintainer but less helpful for reviewers. + +## Decision + +Add a minimal `composer.json` with development-only tools and a single repeatable verification command: + +```bash +composer verify +``` + +`composer verify` runs: + +- `composer lint:php` +- `composer analyse` +- `composer test` +- `composer audit:showcase` + +The live public smoke check stays separate: + +```bash +composer smoke:public +``` + +## Consequences + +GitHub Actions and local reviewers use the same entrypoint. + +The repository now includes a lock file so the public quality toolchain is reproducible. + +Live route checks are not part of required CI because they depend on external network and production availability. diff --git a/docs/adr/0003-shopware-sync-boundary.md b/docs/adr/0003-shopware-sync-boundary.md new file mode 100644 index 0000000..ac3359e --- /dev/null +++ b/docs/adr/0003-shopware-sync-boundary.md @@ -0,0 +1,29 @@ +# ADR 0003: Keep Shopware Sync Admin-Triggered And Server-Side + +## Status + +Accepted + +## Context + +SkillBuilder includes a demo workflow that synchronizes published lessons to Shopware products through the Shopware Admin API. + +The workflow touches external credentials, product state, publication status, and cross-system consistency. + +## Decision + +Keep the Shopware synchronization: + +- admin-triggered +- server-side +- idempotent through stable product numbers +- documented as a portfolio demo boundary +- separate from storefront JavaScript + +Published lessons are synchronized as products. Lesson chapters remain internal SkillBuilder learning structure and are not synchronized as Shopware categories. + +## Consequences + +The integration stays easy to explain and safe to demo. + +Longer-term, if synchronization grows, the next senior step would be to move heavy work behind a queue or retryable job while keeping the admin action as the visible trigger. diff --git a/docs/evidence/public-live-readiness-2026-06-15.md b/docs/evidence/public-live-readiness-2026-06-15.md index 7ff664c..63fdfc0 100644 --- a/docs/evidence/public-live-readiness-2026-06-15.md +++ b/docs/evidence/public-live-readiness-2026-06-15.md @@ -17,6 +17,7 @@ roadynet/skillbuilder-showcase Local checks performed: ```text +composer verify: OK PHP syntax check for examples and tests: OK PHPUnit 11.5.55 on PHP 8.4.21: OK, 7 tests, 10 assertions PHPStan 2.2.2 level max for public examples: OK @@ -44,6 +45,12 @@ Public routes checked without publishing credentials: The canonical public privacy route for this project is `/dsgvo`. +Repeatable command: + +```bash +composer smoke:public +``` + ## Notes - Demo credentials remain unpublished. diff --git a/docs/quality-report.md b/docs/quality-report.md index 93b7f42..f55c821 100644 --- a/docs/quality-report.md +++ b/docs/quality-report.md @@ -5,7 +5,7 @@ Public showcase examples: ```text -phpunit +composer verify 7 tests 10 assertions @@ -30,11 +30,19 @@ phpunit.xml is valid XML Public PHPStan check: ```text -phpstan analyse --configuration=phpstan.neon +composer analyse [OK] No errors ``` +Public live smoke check: + +```text +composer smoke:public + +/health, /login, /register, /impressum, /dsgvo: 200 with security headers +``` + Current private test suite: ```text @@ -98,6 +106,10 @@ Detailed evidence: Related senior review context: - [Senior Symfony review guide](senior-symfony-review.md) +- [Security and privacy boundary](security-privacy.md) +- [ADR 0001: public/private repository split](adr/0001-public-private-repository-split.md) +- [ADR 0002: Composer verify for public showcase checks](adr/0002-composer-verify-for-public-showcase.md) +- [ADR 0003: Shopware sync boundary](adr/0003-shopware-sync-boundary.md) ## Important Finding Fixed diff --git a/docs/security-privacy.md b/docs/security-privacy.md new file mode 100644 index 0000000..de73243 --- /dev/null +++ b/docs/security-privacy.md @@ -0,0 +1,63 @@ +# Security And Privacy Boundary + +This public repository is designed to be safe to share with recruiters, clients, and technical reviewers. + +## Public Repository Rules + +The showcase must not contain: + +- production credentials +- real `.env` files +- real database URLs +- private Symfony source directories +- private deployment scripts +- user data +- generated GDPR exports +- upload directories +- Shopware Admin API credentials + +The repository may contain: + +- placeholder configuration +- sanitized architecture documentation +- representative example code +- screenshots without private user data +- public quality evidence +- public smoke-check results + +## Automated Guards + +`composer verify` runs the public safety audit: + +```bash +python tests/showcase_audit.py +``` + +The audit checks: + +- private application paths are not tracked +- only `.env.example` is allowed +- obvious secret patterns are absent +- `.env.example` uses placeholders +- local Markdown links resolve +- screenshots exist and are PNG files +- public wording stays consistent +- `phpunit.xml` parses as XML + +## Live Demo Boundary + +The public live demo URL can be shown, but demo credentials are not published in this repository. + +The public smoke script checks only unauthenticated routes: + +```bash +composer smoke:public +``` + +Protected route checks and admin walkthroughs should remain private or be demonstrated live without publishing credentials. + +## Shopware Boundary + +Shopware Admin API credentials are server-side only. + +The connected Shopware storefront is a portfolio demo. It does not process real orders, payments, registrations, contact forms, or customer data. diff --git a/docs/senior-symfony-review.md b/docs/senior-symfony-review.md index 2f9a04d..435ed90 100644 --- a/docs/senior-symfony-review.md +++ b/docs/senior-symfony-review.md @@ -13,7 +13,8 @@ Start here: 3. [Quality report](quality-report.md) 4. [Shopware bridge](shopware-bridge.md) 5. [Public live readiness evidence](evidence/public-live-readiness-2026-06-15.md) -6. Representative examples in [`examples/`](../examples/) +6. [Security and privacy boundary](security-privacy.md) +7. Representative examples in [`examples/`](../examples/) The fastest technical read is the combination of the architecture page, the service examples, and the PHPUnit tests. @@ -40,6 +41,7 @@ SkillBuilder is positioned as a backend-driven Symfony business application, not | Secrets stay outside Git | Public showcase has placeholders only; production values live in environment or private config outside the repo tree | [README](../README.md), [.env.example](../.env.example) | | Public repository has its own safety audit | The showcase should prove it is safe to publish, not merely claim it | [`tests/showcase_audit.py`](../tests/showcase_audit.py) | | Static analysis is part of the public proof | PHPStan runs at `level: max` for the representative PHP examples without requiring private code | [`phpstan.neon`](../phpstan.neon), [Quality report](quality-report.md) | +| One command verifies the public evidence | Reviewers can run the same check locally and in CI | [`composer.json`](../composer.json), [ADR 0002](adr/0002-composer-verify-for-public-showcase.md) | ## Symfony-Specific Signals @@ -66,6 +68,7 @@ The project also documents non-glamorous production concerns: - live smoke checks after cache and configuration changes - external secrets file pattern for server-only credentials - public/private repository split +- Composer-based public verification command - evidence notes for checks that are safe to publish This matters because many Symfony failures are operational rather than syntactic: stale containers, miswired services, environment drift, wrong cache state, or credentials leaking into the wrong layer. diff --git a/phpstan.neon b/phpstan.neon index 1bdaa8c..4212d6c 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -2,5 +2,3 @@ parameters: level: max paths: - examples - bootstrapFiles: - - tests/phpstan/phpunit-bootstrap.php diff --git a/tests/php_lint.py b/tests/php_lint.py new file mode 100644 index 0000000..c572995 --- /dev/null +++ b/tests/php_lint.py @@ -0,0 +1,48 @@ +#!/usr/bin/env python3 +"""Run PHP syntax checks for all public example/test PHP files.""" + +from __future__ import annotations + +import subprocess +import sys +from pathlib import Path + + +ROOT = Path(__file__).resolve().parents[1] +SCAN_DIRS = ("examples", "tests") + + +def main() -> None: + files = sorted( + path + for directory in SCAN_DIRS + for path in (ROOT / directory).rglob("*.php") + ) + + if not files: + print("No PHP files found") + return + + failures = [] + + for path in files: + result = subprocess.run( + ["php", "-l", str(path)], + cwd=ROOT, + text=True, + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + check=False, + ) + print(result.stdout.strip()) + if result.returncode != 0: + failures.append(path.relative_to(ROOT).as_posix()) + + if failures: + print("PHP syntax check failed for:") + print("\n".join(failures)) + sys.exit(1) + + +if __name__ == "__main__": + main() diff --git a/tests/phpstan/phpunit-bootstrap.php b/tests/phpstan/phpunit-bootstrap.php deleted file mode 100644 index 535d1c9..0000000 --- a/tests/phpstan/phpunit-bootstrap.php +++ /dev/null @@ -1,26 +0,0 @@ - tuple[int, int, bool, bool, bool]: + request = urllib.request.Request( + BASE_URL + path, + headers={"Cache-Control": "no-cache", "User-Agent": "SkillBuilder-showcase-smoke/1.0"}, + method="GET", + ) + with urllib.request.urlopen(request, timeout=TIMEOUT_SECONDS) as response: + body = response.read() + headers = response.headers + return ( + response.status, + len(body), + bool(headers.get("Content-Security-Policy")), + headers.get("X-Content-Type-Options", "").lower() == "nosniff", + headers.get("X-Frame-Options", "").upper() == "DENY", + ) + + +def main() -> None: + failures: list[str] = [] + + print("path,status,bytes,csp,nosniff,frame") + for path in PATHS: + try: + status, size, has_csp, has_nosniff, has_frame = fetch(path) + except urllib.error.HTTPError as exc: + print(f"{path},{exc.code},0,false,false,false") + failures.append(path) + continue + except Exception as exc: + print(f"{path},0,0,false,false,false ({exc})") + failures.append(path) + continue + + print(f"{path},{status},{size},{has_csp},{has_nosniff},{has_frame}") + if status < 200 or status >= 400 or not has_csp or not has_nosniff or not has_frame: + failures.append(path) + + if failures: + print("Public smoke check failed for: " + ", ".join(failures)) + sys.exit(1) + + +if __name__ == "__main__": + main() diff --git a/tests/showcase_audit.py b/tests/showcase_audit.py index 6951efe..bccd20b 100644 --- a/tests/showcase_audit.py +++ b/tests/showcase_audit.py @@ -7,6 +7,7 @@ import subprocess import sys import xml.etree.ElementTree as ET +from fnmatch import fnmatch from pathlib import Path from urllib.parse import unquote @@ -14,7 +15,7 @@ ROOT = Path(__file__).resolve().parents[1] TEXT_SUFFIXES = {".md", ".php", ".svg", ".txt", ".yml", ".yaml", ".xml"} ALLOWED_ENV_FILES = {Path(".env.example")} -IGNORED_SCAN_DIRS = {".git", "vendor", ".phpunit.cache"} +IGNORED_SCAN_DIRS = {".git", "vendor", ".phpunit.cache", ".phpstan.cache"} def fail(message: str) -> None: @@ -24,8 +25,39 @@ def fail(message: str) -> None: def tracked_files(*patterns: str) -> list[Path]: cmd = ["git", "ls-files", *patterns] - output = subprocess.check_output(cmd, cwd=ROOT, text=True) - return [Path(line) for line in output.splitlines() if line.strip()] + try: + output = subprocess.check_output( + cmd, + cwd=ROOT, + text=True, + stderr=subprocess.DEVNULL, + ) + return [ + Path(line) + for line in output.splitlines() + if line.strip() and (ROOT / line).is_file() + ] + except (FileNotFoundError, subprocess.CalledProcessError): + return filesystem_files(*patterns) + + +def filesystem_files(*patterns: str) -> list[Path]: + files = [] + + for path in ROOT.rglob("*"): + if not path.is_file(): + continue + + relative = path.relative_to(ROOT) + if any(part in IGNORED_SCAN_DIRS for part in relative.parts): + continue + + if patterns and not any(fnmatch(relative.as_posix(), pattern) for pattern in patterns): + continue + + files.append(relative) + + return sorted(files) def read_text(path: Path) -> str: