Move to Motoko

migrating-php-apps · references/inventory.md

Inventory script

An agent skill for moving a PHP website or application to the Internet Computer with a Motoko backend. This is the readable version; agents load the Markdown file.

Install the companion ICP skillsnpx skills add dfinity/icskills

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 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.

#!/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"; DOCROOT="."
# Composer layouts keep the web root in a subdirectory (Drupal's web/ or docroot/, Bedrock's web/)
for d in . web docroot public_html htdocs; do
  { [ -f "$d/wp-config.php" ] || [ -f "$d/wp-includes/version.php" ]; } && { fw="WordPress"; DOCROOT="$d"; }
  [ -f "$d/core/lib/Drupal.php" ] && { fw="Drupal"; DOCROOT="$d"; }
done
[ -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**"
[ "$DOCROOT" != "." ] && echo "- Web root: \`$DOCROOT/\` (Composer layout: the project root holds composer.json, vendor/ and config outside the web root)"
R="."; [ "$DOCROOT" != "." ] && R="./$DOCROOT"
# Skip the platform's own code: it is replaced, not migrated. Your code lives in the custom directories.
CORE=''; CUSTOM=''; ROOTSKIP='^$'
case "$fw" in
  WordPress) CORE="-path $R/wp-admin -prune -o -path $R/wp-includes -prune -o"; CUSTOM="$DOCROOT/wp-content/themes, $DOCROOT/wp-content/plugins, $DOCROOT/wp-content/mu-plugins"; ROOTSKIP="^$R/(wp-[a-z-]+|xmlrpc|index)\\.php\$" ;;
  Drupal) CORE="-path $R/core -prune -o"; CUSTOM="$DOCROOT/modules/custom, $DOCROOT/themes/custom, $DOCROOT/sites/*/modules; files under $DOCROOT/sites/default/files" ;;
  Joomla) CORE='-path ./libraries -prune -o -path ./installation -prune -o'; CUSTOM='templates, plugins, components and administrator/components (your own extensions)' ;;
  "Magento 2") CORE='-path ./lib -prune -o -path ./setup -prune -o -path ./generated -prune -o -path ./dev -prune -o'; CUSTOM='app/code, app/design' ;;
esac
PRUNE="$PRUNE $CORE"
[ -n "$CUSTOM" ] && echo "- Platform core directories are skipped below. Custom code to inventory: $CUSTOM"
# Extensions: every one is a feature-map row (rebuild, replace or drop with consent). Active state needs the platform's CLI.
case "$fw" in
  WordPress)
    for d in plugins mu-plugins themes; do
      [ -d "$DOCROOT/wp-content/$d" ] && echo "- Installed $d: $(find "$DOCROOT/wp-content/$d" -mindepth 1 -maxdepth 1 \( -type d -o -name '*.php' \) ! -name index.php -printf '%f ' 2>/dev/null)"
    done
    echo "  Active ones: \`wp plugin list --status=active\` and \`wp theme list --status=active\`" ;;
  Drupal)
    for d in modules/contrib modules/custom themes/contrib themes/custom; do
      [ -d "$DOCROOT/$d" ] && echo "- Installed $d: $(find "$DOCROOT/$d" -mindepth 1 -maxdepth 1 -type d -printf '%f ' 2>/dev/null)"
    done
    echo "  Enabled ones: \`drush pm:list --status=enabled --type=module\`" ;;
  Joomla)
    echo "- Installed extensions: \`php cli/joomla.php extension:list\` (components in components/ and administrator/components/, plugins in plugins/)" ;;
esac
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):"
eval "find . -maxdepth 2 $PRUNE -name '*.php' -print" 2>/dev/null | grep -v -E "$ROOTSKIP" | sort | sed 's/^/- /'
for f in $(files "-name .htaccess" | grep -v -E "/(wp-content/plugins|wp-content/themes|modules/contrib|vendor)/"); 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