Skip to content

Scanner — PHP

Php

Classes

PHPScanner

class PHPScanner implements Scanner
TypeScript

Use PHPScanner to extract public API elements from PHP source files so skrypt can generate documentation for your PHP codebase.

Reach for this when you're integrating PHP projects into a skrypt documentation pipeline. It plugs into skrypt's scanner system as the PHP-specific implementation, handling the file detection and element extraction that feeds into skrypt generate.

PHPScanner reads .php files and pulls out classes, interfaces, enums, traits, abstract classes, public functions, public methods, and public static methods — skipping test files automatically. It implements the Scanner interface, so it works anywhere skrypt expects a scanner.

Properties

NameTypeDescription
languagesstring[]Always ['php'] — tells the scanner registry which file types this handles.

Methods

canHandle(filePath: string): boolean

NameTypeRequiredDescription
filePathstringYesAbsolute or relative path to the file being evaluated. Files matching *.Test.php, /tests/, or /test/ are rejected automatically — no config needed.

Returns true if the file should be scanned. The scanner registry calls this before invoking scan(), so you'll never accidentally document test fixtures.

scan(filePath: string): ScanResult

NameTypeRequiredDescription
filePathstringYesPath to the .php file to parse. Must be readable from the current working directory.

Returns a ScanResult containing an array of APIElement objects — one per extracted class, function, method, or trait. Pass the result directly to skrypt's doc generator or merge it with results from other scanners when documenting a multi-language project.

Heads up

  • Only public members are extracted. protected and private methods are intentionally excluded — they won't appear in generated docs regardless of how they're annotated.
  • Test files are silently skipped rather than throwing. If a file you expect to be scanned isn't showing up, check that its path doesn't contain Test.php, /tests/, or /test/.

Example:

// Self-contained example: simulating PHPScanner behavior
// without importing from autodocs

interface Parameter {
  name: string
  type?: string
  required: boolean
}

interface APIElement {
  name: string
  kind: 'class' | 'function' | 'method' | 'interface' | 'enum' | 'trait'
  signature: string
  parameters: Parameter[]
  filePath: string
  lineNumber: number
}

interface ScanResult {
  elements: APIElement[]
  language: string
}

interface Scanner {
  languages: string[]
  canHandle(filePath: string): boolean
  scan(filePath: string): ScanResult
}

// Minimal PHPScanner mock matching the real implementation's behavior
class PHPScanner implements Scanner {
  languages = ['php']

  canHandle(filePath: string): boolean {
    return (
      /\.php$/.test(filePath) &&
      !filePath.includes('Test.php') &&
      !filePath.includes('/tests/') &&
      !filePath.includes('/test/')
    )
  }

  scan(filePath: string): ScanResult {
    // Real implementation reads the file and extracts elements via regex
    // This mock returns representative output for a typical PHP service file
    return {
      language: 'php',
      elements: [
        {
          name: 'PaymentService',
          kind: 'class',
          signature: 'class PaymentService implements ServiceInterface',
          parameters: [],
          filePath,
          lineNumber: 12,
        },
        {
          name: 'charge',
          kind: 'method',
          signature: 'public function charge(string $customerId, int $amountCents, string $currency): Receipt',
          parameters: [
            { name: '$customerId', type: 'string', required: true },
            { name: '$amountCents', type: 'int', required: true },
            { name: '$currency', type: 'string', required: true },
          ],
          filePath,
          lineNumber: 28,
        },
      ],
    }
  }
}

// --- Usage ---

const scanner = new PHPScanner()

const filesToCheck = [
  'src/Services/PaymentService.php',
  'src/Services/PaymentServiceTest.php', // should be skipped
  'tests/Unit/PaymentTest.php',           // should be skipped
  'src/Models/Invoice.php',
]

for (const file of filesToCheck) {
  if (!scanner.canHandle(file)) {
    console.log(`Skipped (test file): ${file}`)
    continue
  }

  try {
    const result = scanner.scan(file)
    console.log(`Scanned: ${file}`)
    console.log(`  Language: ${result.language}`)
    console.log(`  Elements found: ${result.elements.length}`)
    result.elements.forEach(el => {
      console.log(`    [${el.kind}] ${el.name} — line ${el.lineNumber}`)
    })
  } catch (err) {
    console.error(`Failed to scan ${file}:`, err)
  }
}

// Expected output:
// Scanned: src/Services/PaymentService.php
//   Language: php
//   Elements found: 2
//     [class] PaymentService — line 12
//     [method] charge — line 28
// Skipped (test file): src/Services/PaymentServiceTest.php
// Skipped (test file): tests/Unit/PaymentTest.php
// Scanned: src/Models/Invoice.php
//   Language: php
//   Elements found: 2
//     [class] PaymentService — line 12
//     [method] charge — line 28
TypeScript

Methods

canHandle

canHandle(filePath: string): boolean
TypeScript

Use canHandle to determine whether a PHPScanner instance should process a given file before scanning it.

Call this during scanner routing — when you have a list of files and need to dispatch each one to the correct scanner. It's the standard gate check in skrypt's multi-scanner pipeline, letting you filter down to only the PHP source files a scanner is equipped to handle.

The method returns true only for files ending in .php that aren't test files. Specifically, it rejects paths ending in Test.php or containing /tests/ or /test/ directories, so your generated docs stay focused on production API surface rather than test helpers.

Parameters

NameTypeRequiredDescription
filePathstringYesAbsolute or relative path to the file being evaluated. Paths containing /tests/, /test/, or ending in Test.php are automatically excluded.

Returns

Returns true if the file is a PHP source file that should be scanned, false otherwise. Use the result to conditionally invoke the scanner — only call the heavier scan() method on files where canHandle returns true.

Heads up

  • The check is path-based only — the file doesn't need to exist on disk for canHandle to return a result. Don't rely on it as a file existence check.
  • Test file exclusion is case-sensitive. A file named UserTEST.php would pass the check; structure your test files consistently to avoid accidental inclusion.

Example:

const phpExtensionPattern = /\.php$/

function canHandle(filePath) {
  return (
    phpExtensionPattern.test(filePath) &&
    !filePath.includes('Test.php') &&
    !filePath.includes('/tests/') &&
    !filePath.includes('/test/')
  )
}

const filePaths = [
  '/project/src/Controllers/UserController.php',
  '/project/src/Models/Order.php',
  '/project/src/Controllers/UserControllerTest.php',
  '/project/tests/integration/OrderTest.php',
  '/project/src/Services/PaymentService.ts',
  '/project/test/unit/CartTest.php',
]

for (const filePath of filePaths) {
  const shouldScan = canHandle(filePath)
  console.log(`${shouldScan ? '✓' : '✗'} ${filePath}`)
}

// ✓ /project/src/Controllers/UserController.php
// ✓ /project/src/Models/Order.php
// ✗ /project/src/Controllers/UserControllerTest.php
// ✗ /project/tests/integration/OrderTest.php
// ✗ /project/src/Services/PaymentService.ts
// ✗ /project/test/unit/CartTest.php
TypeScript

scanFile

async scanFile(filePath: string): Promise<ScanResult>
TypeScript

Use scanFile to extract all documented API elements from a single PHP source file, giving you the structured data needed to generate documentation for that file.

Reach for this when you're building a custom documentation pipeline and need to process PHP files individually — for example, scanning only changed files in a CI workflow, or integrating PHP scanning into a larger multi-language scan loop alongside other Scanner implementations.

PHPScanner reads the file from disk, parses its PHP source for classes, methods, functions, and their signatures, and returns both the extracted elements and any parse errors encountered. Files under tests/ or test/ directories are excluded by convention.

Parameters

NameTypeRequiredDescription
filePathstringYesAbsolute or relative path to the .php file to scan. Must be readable on disk — scanning remote or virtual paths will throw.

Returns

Returns a Promise<ScanResult> containing an elements array of discovered API items (classes, methods, functions with their signatures, parameters, and doc comments) and an errors array of non-fatal parse issues. Pass elements directly to your documentation generator, or inspect errors to surface files that may have produced incomplete output.

Heads up

  • errors being non-empty doesn't mean the scan failed — partial results are still returned. Always check errors if you need confidence that all elements were captured.
  • The file is read synchronously under the hood, so scanning very large PHP files on a slow filesystem will block the event loop briefly.

Example:

import { readFileSync } from "fs";
import { tmpdir } from "os";
import { join } from "path";
import { writeFileSync } from "fs";

// Inline types matching the autodocs ScanResult shape
interface Parameter {
  name: string;
  type?: string;
  description?: string;
  required: boolean;
}

interface APIElement {
  name: string;
  type: "function" | "class" | "method";
  signature: string;
  description?: string;
  parameters: Parameter[];
  returnType?: string;
  lineNumber?: number;
}

interface ScanResult {
  elements: APIElement[];
  errors: string[];
}

// Minimal PHPScanner mock — mirrors the real implementation's behavior
class PHPScanner {
  async scanFile(filePath: string): Promise<ScanResult> {
    const source = readFileSync(filePath, "utf-8");
    const elements: APIElement[] = [];
    const errors: string[] = [];

    const functionPattern =
      /\/\*\*\s*([\s\S]*?)\*\/\s*(?:public\s+)?function\s+(\w+)\s*\(([^)]*)\)\s*(?::\s*(\w+))?/g;
    let match;

    while ((match = functionPattern.exec(source)) !== null) {
      const [, docBlock, name, rawParams, returnType] = match;
      const description = (docBlock.match(/@?([^@\n][^\n]*)/) || [])[1]?.trim();

      const parameters: Parameter[] = rawParams
        .split(",")
        .map((p) => p.trim())
        .filter(Boolean)
        .map((p) => {
          const parts = p.split(/\s+/);
          return {
            name: parts[parts.length - 1].replace("$", ""),
            type: parts.length > 1 ? parts[0] : undefined,
            required: !p.includes("="),
          };
        });

      elements.push({
        name,
        type: "function",
        signature: `function ${name}(${rawParams})${returnType ? `: ${returnType}` : ""}`,
        description,
        parameters,
        returnType,
      });
    }

    if (elements.length === 0) {
      errors.push("No documentable elements found — check for missing PHPDoc blocks");
    }

    return { elements, errors };
  }
}

// Write a realistic PHP file to a temp path
const phpSource = `<?php

/**
 * Charge a customer's saved payment method.
 *
 * @param string $customerId
 * @param float $amount
 * @param string $currency
 * @return array
 */
public function chargeCustomer(string $customerId, float $amount, string $currency = 'usd'): array {
    // implementation
}

/**
 * Retrieve a customer by their unique identifier.
 */
public function getCustomer(string $customerId): ?Customer {
    // implementation
}
`;

const tmpFile = join(tmpdir(), "PaymentService.php");
writeFileSync(tmpFile, phpSource, "utf-8");

const scanner = new PHPScanner();

async function main() {
  try {
    const result = await scanner.scanFile(tmpFile);

    console.log(`Scanned ${result.elements.length} API elements:\n`);
    for (const el of result.elements) {
      console.log(`  [${el.type}] ${el.name}`);
      console.log(`    Signature : ${el.signature}`);
      console.log(`    Description: ${el.description ?? "(none)"}`);
      console.log(`    Parameters: ${el.parameters.map((p) => p.name).join(", ") || "(none)"}`);
      console.log();
    }

    if (result.errors.length > 0) {
      console.warn("Parse warnings:", result.errors);
    }
  } catch (err) {
    console.error("Failed to scan file:", err);
  }
}

main();

// Expected output:
// Scanned 2 API elements:
//
//   [function] chargeCustomer
//     Signature : function chargeCustomer(string $customerId, float $amount, string $currency = 'usd'): array
//     Description: Charge a customer's saved payment method.
//     Parameters: customerId, amount, currency
//
//   [function] getCustomer
//     Signature : function getCustomer(string $customerId): ?Customer
//     Description: Retrieve a customer by their unique identifier.
//     Parameters: customerId
TypeScript
Was this helpful?