# PHP Inventory Script

Read-only. Run it at the root of the PHP project (or pass the path) and keep the output with the migration notes. It never prints secret values: for configuration files it lists constant and key **names** only.

```bash
bash php-inventory.sh /path/to/php-site > inventory.md
```

Requires `bash`, `find`, `grep`, `du`, `xargs`; `python3` for the Composer summary. Tested on a plain-PHP fixture with `.htaccess` rewrites, sessions, password logins, uploads, `mail()`, Guzzle, GD, cron and a SQL dump.

```bash
#!/usr/bin/env bash
# Inventory a PHP codebase before moving it to the Internet Computer.
# Read-only. Prints Markdown. Never prints secret values, only file and key names.
# Usage: bash php-inventory.sh [path-to-php-repo] > inventory.md
set -u
ROOT="${1:-.}"
cd "$ROOT" || exit 1
PRUNE='-path ./vendor -prune -o -path ./node_modules -prune -o -path ./.git -prune -o'

files() { eval "find . $PRUNE -type f \\( $1 \\) -print" 2>/dev/null; }
php_files() { files "-name '*.php' -o -name '*.phtml' -o -name '*.inc'"; }
hits() { # $1 = label, $2 = extended regex
  local n
  n=$(php_files | xargs -r grep -l -E "$2" 2>/dev/null | wc -l | tr -d ' ')
  printf '| %s | %s |\n' "$1" "$n"
}

echo "# PHP inventory: $(basename "$(pwd)")"
echo
echo "## Platform"
fw="plain PHP"
[ -f wp-config.php ] || [ -f wp-includes/version.php ] && fw="WordPress"
[ -f core/lib/Drupal.php ] && fw="Drupal"
[ -f libraries/src/Version.php ] || [ -f administrator/manifests/files/joomla.xml ] && fw="Joomla"
[ -f bin/magento ] && [ -f app/etc/env.php ] && fw="Magento 2"
[ -f artisan ] && grep -q '"laravel/framework"' composer.json 2>/dev/null && fw="Laravel"
[ -f bin/console ] && grep -q '"symfony/framework-bundle"' composer.json 2>/dev/null && fw="Symfony"
[ -f spark ] || [ -f system/CodeIgniter.php ] && fw="CodeIgniter"
echo "- Detected: **$fw**"
echo "- PHP files (excluding vendor/): $(php_files | wc -l | tr -d ' '), lines: $(php_files | xargs -r cat 2>/dev/null | wc -l | tr -d ' ')"
if [ -f composer.json ] && command -v python3 >/dev/null; then
  python3 - <<'PY'
import json
c = json.load(open("composer.json"))
req = c.get("require", {})
print("- PHP constraint: " + req.get("php", "(none)"))
deps = [k for k in req if k != "php" and not k.startswith("ext-")]
exts = [k for k in req if k.startswith("ext-")]
print("- Composer packages: " + (", ".join(deps) if deps else "(none)"))
if exts: print("- Required PHP extensions: " + ", ".join(exts))
PY
fi

echo
echo "## Entry points and URLs"
echo "PHP files in the top two levels (likely entry points):"
find . -maxdepth 2 -path ./vendor -prune -o -name '*.php' -print 2>/dev/null | sort | sed 's/^/- /'
for f in $(files "-name .htaccess"); do
  echo; echo "Rewrite rules in \`$f\`:"; grep -E '^\s*(RewriteRule|Redirect|RedirectMatch)' "$f" | sed 's/^/    /'
done
[ -d routes ] && { echo; echo "Laravel route files:"; ls routes | sed 's/^/- routes\//'; }
[ -d config ] && ls config/routes* >/dev/null 2>&1 && { echo; echo "Symfony route config:"; ls -d config/routes* | sed 's/^/- /'; }

echo
echo "## Features that need a decision"
echo "| Feature (files using it) | Files |"
echo "|---|---|"
hits 'Sessions ($_SESSION, session_start)' '\$_SESSION|session_start\('
hits 'Password logins (password_verify, crypt, md5/sha1 on passwords)' 'password_verify|password_hash|crypt\(|md5\(|sha1\('
hits 'Form posts ($_POST)' '\$_POST'
hits 'Query parameters ($_GET, $_REQUEST)' '\$_GET|\$_REQUEST'
hits 'File uploads ($_FILES, move_uploaded_file)' '\$_FILES|move_uploaded_file'
hits 'Cookies (setcookie, $_COOKIE)' 'setcookie\(|\$_COOKIE'
hits 'Email (mail(), PHPMailer, Swift, Symfony Mailer)' '\bmail\(|PHPMailer|Swift_|Mailer'
hits 'Outbound HTTP (curl, Guzzle, file_get_contents http)' 'curl_init|GuzzleHttp|file_get_contents\(.https?:|fsockopen'
hits 'Shell commands (exec, shell_exec, system, proc_open)' '(^|[^>:a-zA-Z0-9_$])(exec|shell_exec|system|passthru|proc_open|popen)\('
hits 'Image processing (GD, Imagick)' 'imagecreate|Imagick|getimagesize'
hits 'PDF / spreadsheet generation' 'TCPDF|FPDF|Dompdf|Mpdf|PhpSpreadsheet'
hits 'Database access (PDO, mysqli)' 'new PDO|mysqli_|->query\(|->prepare\('
hits 'Redirect headers (header Location)' "header\\(['\"]Location"

echo
echo "## Database"
for f in $(files "-name '*.sql'"); do
  echo "Tables in \`$f\`:"
  grep -i -o -E 'CREATE TABLE (IF NOT EXISTS )?`?[A-Za-z0-9_]+`?' "$f" | sed -E 's/.*TABLE (IF NOT EXISTS )?//I; s/`//g; s/^/- /'
done
[ -d database/migrations ] && echo "- Laravel migrations: $(ls database/migrations | wc -l | tr -d ' ') files in database/migrations"
[ -d src/Entity ] && echo "- Doctrine entities: $(ls src/Entity | wc -l | tr -d ' ') files in src/Entity"
echo "(Get row counts and sizes from the live database: see the skill's export step.)"

echo
echo "## Scheduled jobs"
for f in $(files "-iname '*cron*' -o -name '*.crontab'"); do echo "- \`$f\`"; done
php_files | xargs -r grep -l -E 'wp_schedule_event|->(daily|hourly|everyMinute|cron)\(|AsCronTask' 2>/dev/null | sed 's/^/- scheduler code: /'

echo
echo "## Files on disk"
for d in uploads wp-content/uploads sites/default/files public/storage storage/app/public pub/media images media files; do
  [ -d "$d" ] && echo "- \`$d\`: $(du -sh "$d" | cut -f1), $(find "$d" -type f | wc -l | tr -d ' ') files"
done

echo
echo "## Configuration and secrets (names only, never values)"
for f in config.php wp-config.php .env .env.local app/etc/env.php sites/default/settings.php configuration.php config/parameters.yml; do
  [ -f "$f" ] || continue
  echo "- \`$f\`:"
  { grep -o -E "define\\(['\"][A-Za-z0-9_]+['\"]" "$f" | sed -E "s/define\\(['\"]//; s/['\"]$//"
    grep -o -E '^[A-Z][A-Z0-9_]*=' "$f" | tr -d '='
    grep -o -E "public \\\$[a-z_]+" "$f" | sed 's/public \$//'
  } 2>/dev/null | sort -u | tr '\n' ' ' | sed 's/^/    /'; echo
done
```

## Reading the output

- **Platform** decides which section of `frameworks.md` to read next.
- **Rewrite rules** are the old URL map. Every rule becomes a row in the feature map and, later, an old-URL test (`old-urls.md`).
- **Features that need a decision** are file counts from text search. They are leads, not proof: open each file. A count of 0 is not a guarantee either (code can be generated, included dynamically or live in `vendor/`).
- **Database**: table names only. Get row counts and sizes from the live database (SKILL.md, Phase 1).
- **Configuration and secrets**: these names tell you which integrations exist (SMTP, payment, API keys). Do not open the values in chat or copy them anywhere.
